def lower_convex_hull(global_grid, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    indep_conds = sorted([x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    comp_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    pot_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError('Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        comp_values = cartesian([result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = {x[2:] for x in comp_conds}
        dependent_component = set(result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError('Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(dependent_component[0])
        comp_values = np.concatenate((comp_values[..., :insert_idx],
                                      1 - np.sum(comp_values, keepdims=True, axis=-1),
                                      comp_values[..., insert_idx:]),
                                     axis=-1)
        # Prevent compositions near an edge from going negative
        comp_values[np.nonzero(comp_values < MIN_SITE_FRACTION)] = MIN_SITE_FRACTION*10
        # TODO: Assumes N=1
        comp_values /= comp_values.sum(axis=-1, keepdims=True)
        #print(comp_values)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    # factored out via profiling
    result_array_GM_values = result_array.GM.values
    result_array_points_values = result_array.points.values
    result_array_MU_values = result_array.MU.values
    result_array_NP_values = result_array.NP.values
    result_array_X_values = result_array.X.values
    result_array_Y_values = result_array.Y.values
    result_array_Phase_values = result_array.Phase.values
    global_grid_GM_values = global_grid.GM.values
    global_grid_X_values = global_grid.X.values
    num_comps = result_array.dims['component']

    it = np.nditer(result_array_GM_values, flags=['multi_index'])
    comp_coord_shape = tuple(len(result_array.coords[cond]) for cond in comp_conds)
    while not it.finished:
        indep_idx = it.multi_index[:len(indep_conds)]
        if len(comp_conds) > 0:
            comp_idx = np.ravel_multi_index(it.multi_index[len(indep_conds):], comp_coord_shape)
            idx_comp_values = comp_values[comp_idx]
        else:
            idx_comp_values = np.atleast_1d(1.)
        idx_global_grid_X_values = global_grid_X_values[indep_idx]
        idx_global_grid_GM_values = global_grid_GM_values[indep_idx]
        idx_result_array_MU_values = result_array_MU_values[it.multi_index]
        idx_result_array_NP_values = result_array_NP_values[it.multi_index]
        idx_result_array_GM_values = result_array_GM_values[it.multi_index]
        idx_result_array_points_values = result_array_points_values[it.multi_index]
        result_array_GM_values[it.multi_index] = \
            hyperplane(idx_global_grid_X_values, idx_global_grid_GM_values,
                       idx_comp_values, idx_result_array_MU_values,
                       idx_result_array_NP_values, idx_result_array_points_values)
        # Copy phase values out
        points = result_array_points_values[it.multi_index]
        result_array_Phase_values[it.multi_index][:num_comps] = global_grid.Phase.values[indep_idx].take(points, axis=0)[:num_comps]
        result_array_X_values[it.multi_index][:num_comps] = global_grid.X.values[indep_idx].take(points, axis=0)[:num_comps]
        result_array_Y_values[it.multi_index][:num_comps] = global_grid.Y.values[indep_idx].take(points, axis=0)[:num_comps]
        # Special case: Sometimes fictitious points slip into the result
        # This can happen when we calculate stoichimetric phases by themselves
        if '_FAKE_' in result_array_Phase_values[it.multi_index]:
            # Chemical potentials are meaningless in this case
            idx_result_array_MU_values[...] = 0
            new_energy = 0.
            molesum = 0.
            for idx in range(len(result_array_Phase_values[it.multi_index])):
                midx = it.multi_index + (idx,)
                if result_array_Phase_values[midx] == '_FAKE_':
                    result_array_Phase_values[midx] = ''
                    result_array_X_values[midx] = np.nan
                    result_array_Y_values[midx] = np.nan
                    idx_result_array_NP_values[idx] = np.nan
                else:
                    new_energy += idx_result_array_NP_values[idx] * global_grid.GM.values[np.index_exp[indep_idx + (points[idx],)]]
                    molesum += idx_result_array_NP_values[idx]
            result_array_GM_values[it.multi_index] = new_energy / molesum
        it.iternext()
    del result_array['points']
    return result_array
Beispiel #2
0
def lower_convex_hull(global_grid, result_array, verbose=False):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.
    verbose : bool
        Display details to stdout. Useful for debugging.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [x for x in result_array.coords.keys() if x not in ['vertex',
                                                                     'component']]
    indep_conds = sorted([x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']),)
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape if this is the first iteration
    if result_array.attrs['hull_iterations'] == 1:
        result_array['points'] = result_array['points'].transpose(*(conditions + ['vertex']))
        result_array['GM'] = result_array['GM'].transpose(*conditions)
        result_array['NP'] = result_array['NP'].transpose(*(conditions + ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError('Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian([result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError('Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(dependent_component[0])
        comp_values = np.concatenate((comp_values[..., :insert_idx],
                                      1 - np.sum(comp_values, keepdims=True, axis=-1),
                                      comp_values[..., insert_idx:]),
                                     axis=-1)
        # Prevent compositions near an edge from going negative
        comp_values[np.nonzero(comp_values < MIN_SITE_FRACTION)] = MIN_SITE_FRACTION*10
        # TODO: Assumes N=1
        comp_values /= comp_values.sum(axis=-1, keepdims=True)
        #print(comp_values)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape + (len(global_grid.points),),
                                   dtype=np.float)

    max_iterations = 200
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(result_array.MU.values.shape)),
                                                 result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[flat_statevar_indices +
                                                         (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T,)]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1,) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(np.linalg.svd(trial_matrix,
                                                     compute_uv=False) > 1e-09,
                                       axis=-1, keepdims=True)
        #('NONDEGENERATE INDICES', nondegenerate_indices)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        #sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        #index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
        #                        sum_array)
        index_array = np.arange(trial_matrix.shape[0], dtype=np.int)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        fractions = np.full(result_array['points'].values.shape + \
                            (result_array['points'].values.shape[-1],), -1.)
        fractions[np.unravel_index(index_array, fractions.shape[:-1])] = \
            stacked_lstsq(np.swapaxes(trial_matrix[index_array], -2, -1),
                          comp_values[comp_indices])
        fractions /= fractions.sum(axis=-1, keepdims=True)
        #print('fractions', fractions)
        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are non-negative.
        bounding_indices = np.all(fractions >= -MIN_SITE_FRACTION*100, axis=-1)
        #print('BOUNDING INDICES', bounding_indices)
        if ~np.any(bounding_indices):
            raise ValueError('Desired composition is not inside any candidate simplex. This is a bug.')
        multiple_success_trials = np.sum(bounding_indices, axis=-1, dtype=np.int, keepdims=False) != 1
        #print('MULTIPLE SUCCESS TRIALS SHAPE', np.nonzero(multiple_success_trials))
        if np.any(multiple_success_trials):
            saved_trial = np.zeros_like(multiple_success_trials, dtype=np.int)
            # Case of only degenerate simplices (zero bounding)
            # Choose trial with "least negative" fraction
            zero_success_indices = np.logical_and(~nondegenerate_indices.reshape(bounding_indices.shape),
                                                  multiple_success_trials[..., np.newaxis])
            saved_trial[np.nonzero(zero_success_indices.any(axis=-1))] = \
                np.argmax(fractions[np.nonzero(zero_success_indices.any(axis=-1))].min(axis=-1), axis=-1)
            # Case of multiple bounding non-degenerate simplices
            # Choose the first one. This addresses gh-28.
            multiple_bounding_indices = \
                np.logical_and(np.logical_and(bounding_indices, nondegenerate_indices.reshape(bounding_indices.shape)),
                               multiple_success_trials[..., np.newaxis])
            #print('MULTIPLE SUCCESS TRIALS.shape', multiple_success_trials.shape)
            #print('BOUNDING INDICES.shape', bounding_indices.shape)
            #print('MULTIPLE_BOUNDING_INDICES.shape', multiple_bounding_indices.shape)
            saved_trial[np.nonzero(multiple_bounding_indices.any(axis=-1))] = \
                np.argmax(multiple_bounding_indices[np.nonzero(multiple_bounding_indices.any(axis=-1))], axis=-1)
            #print('SAVED TRIAL.shape', saved_trial.shape)
            #print('BOUNDING INDICES BEFORE', bounding_indices)
            bounding_indices[np.nonzero(multiple_success_trials)] = False
            #print('BOUNDING INDICES FALSE', bounding_indices)
            bounding_indices[np.nonzero(multiple_success_trials) + \
                             (saved_trial[np.nonzero(multiple_success_trials)],)] = True
            #print('BOUNDING INDICES AFTER', bounding_indices)
        fractions.shape = (-1, fractions.shape[-1])
        bounding_indices.shape = (-1,)
        index_array = np.arange(trial_matrix.shape[0], dtype=np.int)[bounding_indices]

        raveled_simplices = trial_simplices.reshape((-1,) + trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(index_array, trial_simplices.shape[:-1]
                                            )[:len(indep_conds)+len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices + (candidate_simplices.T,)].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[statevar_indices + (candidate_simplices,)]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = stacked_lstsq(aligned_compositions.astype(np.float, copy=False),
                                             aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(candidate_potentials,
                                         comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty([int(trial_matrix.shape[0] / trial_shape[0]),
                                      trial_shape[0]])
        if comparison_matrix.shape[0] != aligned_compositions.shape[0]:
            raise ValueError('Arrays have become misaligned. This is a bug. Try perturbing your composition conditions '
                             'by a small amount (1e-4). If you would like, you can report this issue to the development'
                             ' team and they will fix it for future versions.')
        comparison_matrix.fill(np.inf)
        comparison_matrix[np.divide(index_array, trial_shape[0]).astype(np.int),
                          np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(comparison_matrix[calculated_indices],
                                          axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        # This has to be greater-than-or-equal because, in the case where
        # the desired condition is right on top of a simplex vertex (gh-28), there
        # will be no change in energy changing a "_FAKE_" vertex to a real one.
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] <= \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[np.nonzero(bounding_indices)][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:]],
                    out=driving_forces)


        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    if verbose:
        print('Max hull iterations exceeded. Remaining driving force: ', driving_forces.max())
Beispiel #3
0
def lower_convex_hull(global_grid, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [
        x for x in result_array.coords.keys()
        if x not in ['vertex', 'component']
    ]
    indep_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']), )
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape
    result_array['points'] = result_array['points'].transpose(*(conditions +
                                                                ['vertex']))
    result_array['GM'] = result_array['GM'].transpose(*(conditions))
    result_array['NP'] = result_array['NP'].transpose(*(conditions +
                                                        ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError(
            'Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian(
            [result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(
            result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError(
                'Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(
            dependent_component[0])
        comp_values = np.concatenate(
            (comp_values[..., :insert_idx],
             1 - np.sum(comp_values, keepdims=True, axis=-1),
             comp_values[..., insert_idx:]),
            axis=-1)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape +
                              (len(global_grid.points), ),
                              dtype=np.float)

    max_iterations = 50
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[
            ..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(
            np.arange(np.multiply.reduce(result_array.MU.values.shape)),
            result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[
            flat_statevar_indices +
            (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T, )]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1, ) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(
            np.linalg.svd(trial_matrix, compute_uv=False) > 1e-12,
            axis=-1,
            keepdims=True)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
                                sum_array)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(
            index_array, comp_shape)[len(indep_conds) + len(pot_conds)]

        fractions = np.linalg.solve(
            np.swapaxes(trial_matrix[index_array], -2, -1),
            comp_values[comp_indices])

        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are positive.
        bounding_indices = np.all(fractions >= 0, axis=-1)
        index_array = np.atleast_1d(index_array[bounding_indices])

        raveled_simplices = trial_simplices.reshape((-1, ) +
                                                    trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(
            index_array,
            trial_simplices.shape[:-1])[:len(indep_conds) + len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices +
                                                 (candidate_simplices.T, )].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[
            statevar_indices + (candidate_simplices, )]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = np.linalg.solve(
            aligned_compositions.astype(np.float, copy=False),
            aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(
            index_array, comp_shape)[len(indep_conds) + len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(
            candidate_potentials, comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty(
            [trial_matrix.shape[0] / trial_shape[0], trial_shape[0]])
        assert comparison_matrix.shape[0] == aligned_compositions.shape[0]
        comparison_matrix.fill(np.inf)
        comparison_matrix[
            np.divide(index_array, trial_shape[0]).astype(np.int),
            np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(
            comparison_matrix[calculated_indices], axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(
            comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] < \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[
            final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[
            is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[bounding_indices][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] +
                                       ((np.newaxis, ) * len(comp_conds)) +
                                       np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] +
                                          ((np.newaxis, ) * len(comp_conds)) +
                                          np.index_exp[:]],
                    out=driving_forces)

        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    #raise ValueError
    print('Iterations exceeded. Remaining driving force: ',
          driving_forces.max())
    logger.error('Iterations exceeded')
Beispiel #4
0
def lower_convex_hull(global_grid, state_variables, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    state_variables : List[v.StateVariable]
        A list of the state variables (e.g., P, T) used in this calculation.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    state_variables = sorted(state_variables, key=str)
    comp_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_conds_indices = sorted([
        idx for idx, x in enumerate(sorted(result_array.coords['component']))
        if 'X_' + x in comp_conds
    ])
    comp_conds_indices = np.array(comp_conds_indices, dtype=np.uint64)
    pot_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    pot_conds_indices = sorted([
        idx for idx, x in enumerate(sorted(result_array.coords['component']))
        if 'MU_' + x in pot_conds
    ])
    pot_conds_indices = np.array(pot_conds_indices, dtype=np.uint64)

    if len(set(pot_conds_indices) & set(comp_conds_indices)) > 0:
        raise ValueError(
            'Cannot specify component chemical potential and amount simultaneously'
        )

    if len(comp_conds) > 0:
        cart_values = cartesian(
            [result_array.coords[cond] for cond in comp_conds])
    else:
        cart_values = np.atleast_2d(1.)
    # TODO: Handle W(comp) as well as X(comp) here
    comp_values = np.zeros(cart_values.shape[:-1] +
                           (len(result_array.coords['component']), ))
    for idx in range(comp_values.shape[-1]):
        if idx in comp_conds_indices:
            comp_values[..., idx] = cart_values[...,
                                                np.where(comp_conds_indices ==
                                                         idx)[0][0]]
        elif idx in pot_conds_indices:
            # Composition value not used
            comp_values[..., idx] = 0
        else:
            # Dependent component (composition value not used)
            comp_values[..., idx] = 0
    # Prevent compositions near an edge from going negative
    comp_values[np.nonzero(
        comp_values < MIN_SITE_FRACTION)] = MIN_SITE_FRACTION * 10

    if len(pot_conds) > 0:
        cart_pot_values = cartesian(
            [result_array.coords[cond] for cond in pot_conds])

    #result_array['Phase'] = force_indep_align(result_array.Phase)
    # factored out via profiling
    result_array_GM_values = result_array.GM
    result_array_GM_dims = result_array.data_vars['GM'][0]
    result_array_points_values = result_array.points
    result_array_MU_values = result_array.MU
    result_array_NP_values = result_array.NP
    result_array_X_values = result_array.X
    result_array_Y_values = result_array.Y
    result_array_Phase_values = result_array.Phase
    global_grid_GM_values = global_grid.GM
    global_grid_X_values = global_grid.X
    global_grid_Y_values = global_grid.Y
    global_grid_Phase_values = global_grid.Phase
    num_comps = len(result_array.coords['component'])

    it = np.nditer(result_array_GM_values, flags=['multi_index'])
    comp_coord_shape = tuple(
        len(result_array.coords[cond]) for cond in comp_conds)
    pot_coord_shape = tuple(
        len(result_array.coords[cond]) for cond in pot_conds)
    while not it.finished:
        indep_idx = []
        # Relies on being ordered
        for sv in state_variables:
            if str(sv) in result_array.coords.keys():
                coord_idx = list(result_array.coords.keys()).index(str(sv))
                indep_idx.append(it.multi_index[coord_idx])
            else:
                # free state variable
                indep_idx.append(0)
        indep_idx = tuple(indep_idx)
        if len(comp_conds) > 0:
            comp_idx = np.ravel_multi_index(
                tuple(idx
                      for idx, key in zip(it.multi_index, result_array_GM_dims)
                      if key in comp_conds), comp_coord_shape)
            idx_comp_values = comp_values[comp_idx, :]
        else:
            idx_comp_values = np.atleast_1d(1.)
        if len(pot_conds) > 0:
            pot_idx = np.ravel_multi_index(
                tuple(idx
                      for idx, key in zip(it.multi_index, result_array_GM_dims)
                      if key in pot_conds), pot_coord_shape)
            idx_pot_values = np.array(cart_pot_values[pot_idx, :])

        idx_global_grid_X_values = global_grid_X_values[indep_idx]
        idx_global_grid_GM_values = global_grid_GM_values[indep_idx]
        idx_result_array_MU_values = result_array_MU_values[it.multi_index]
        idx_result_array_MU_values[:] = 0
        for idx in range(len(pot_conds_indices)):
            idx_result_array_MU_values[
                pot_conds_indices[idx]] = idx_pot_values[idx]
        idx_result_array_NP_values = result_array_NP_values[it.multi_index]
        idx_result_array_points_values = result_array_points_values[
            it.multi_index]
        result_array_GM_values[it.multi_index] = \
            hyperplane(idx_global_grid_X_values, idx_global_grid_GM_values,
                       idx_comp_values, idx_result_array_MU_values, float(global_grid.coords['N'][0]),
                       pot_conds_indices, comp_conds_indices,
                       idx_result_array_NP_values, idx_result_array_points_values)
        # Copy phase values out
        points = result_array_points_values[it.multi_index]
        result_array_Phase_values[
            it.multi_index][:num_comps] = global_grid_Phase_values[
                indep_idx].take(points, axis=0)[:num_comps]
        result_array_X_values[
            it.multi_index][:num_comps] = global_grid_X_values[indep_idx].take(
                points, axis=0)[:num_comps]
        result_array_Y_values[
            it.multi_index][:num_comps] = global_grid_Y_values[indep_idx].take(
                points, axis=0)[:num_comps]
        # Special case: Sometimes fictitious points slip into the result
        if '_FAKE_' in result_array_Phase_values[it.multi_index]:
            new_energy = 0.
            molesum = 0.
            for idx in range(len(result_array_Phase_values[it.multi_index])):
                midx = it.multi_index + (idx, )
                if result_array_Phase_values[midx] == '_FAKE_':
                    result_array_Phase_values[midx] = ''
                    result_array_X_values[midx] = np.nan
                    result_array_Y_values[midx] = np.nan
                    idx_result_array_NP_values[idx] = np.nan
                else:
                    new_energy += idx_result_array_NP_values[
                        idx] * global_grid.GM[np.index_exp[indep_idx +
                                                           (points[idx], )]]
                    molesum += idx_result_array_NP_values[idx]
            result_array_GM_values[it.multi_index] = new_energy / molesum
        it.iternext()
    result_array.remove('points')
    return result_array
Beispiel #5
0
def lower_convex_hull(global_grid, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [x for x in result_array.coords.keys() if x not in ['vertex',
                                                                     'component']]
    indep_conds = sorted([x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']),)
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape
    result_array['points'] = result_array['points'].transpose(*(conditions + ['vertex']))
    result_array['GM'] = result_array['GM'].transpose(*(conditions))
    result_array['NP'] = result_array['NP'].transpose(*(conditions + ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError('Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian([result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError('Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(dependent_component[0])
        comp_values = np.concatenate((comp_values[..., :insert_idx],
                                      1 - np.sum(comp_values, keepdims=True, axis=-1),
                                      comp_values[..., insert_idx:]),
                                     axis=-1)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape + (len(global_grid.points),),
                                   dtype=np.float)

    max_iterations = 50
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(result_array.MU.values.shape)),
                                                 result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[flat_statevar_indices +
                                                         (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T,)]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1,) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(np.linalg.svd(trial_matrix,
                                                     compute_uv=False) > 1e-12,
                                       axis=-1, keepdims=True)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
                                sum_array)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]

        fractions = np.linalg.solve(np.swapaxes(trial_matrix[index_array], -2, -1),
                                    comp_values[comp_indices])

        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are positive.
        bounding_indices = np.all(fractions >= 0, axis=-1)
        index_array = np.atleast_1d(index_array[bounding_indices])

        raveled_simplices = trial_simplices.reshape((-1,) + trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(index_array, trial_simplices.shape[:-1]
                                            )[:len(indep_conds)+len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices + (candidate_simplices.T,)].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[statevar_indices + (candidate_simplices,)]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = np.linalg.solve(aligned_compositions.astype(np.float, copy=False),
                                               aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(candidate_potentials,
                                         comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty([trial_matrix.shape[0] / trial_shape[0],
                                      trial_shape[0]])
        assert comparison_matrix.shape[0] == aligned_compositions.shape[0]
        comparison_matrix.fill(np.inf)
        comparison_matrix[np.divide(index_array, trial_shape[0]).astype(np.int),
                          np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(comparison_matrix[calculated_indices],
                                          axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] < \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[bounding_indices][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:]],
                    out=driving_forces)


        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    #raise ValueError
    print('Iterations exceeded. Remaining driving force: ', driving_forces.max())
    logger.error('Iterations exceeded')