def _calculate_binary_volume_curve(self): """ Take stable compositions and volume and calculate volume expansion per "B" in AB binary. """ stable_comp = get_array_from_cursor(self.hull_cursor, 'concentration') stable_comp, unique_comp_inds = np.unique(stable_comp, return_index=True) for doc in self.hull_cursor: if 'cell_volume_per_b' not in doc: raise RuntimeError( "Document missing key `cell_volume_per_b`: {}".format(doc)) stable_stoichs = get_array_from_cursor( self.hull_cursor, 'stoichiometry')[unique_comp_inds] stable_vol = get_array_from_cursor( self.hull_cursor, 'cell_volume_per_b')[unique_comp_inds] stable_cap = get_array_from_cursor( self.hull_cursor, 'gravimetric_capacity')[unique_comp_inds] hull_distances = get_array_from_cursor( self.hull_cursor, 'hull_distance')[unique_comp_inds] with np.errstate(divide='ignore'): stable_x = stable_comp / (1 - stable_comp) non_nans = np.argwhere(np.isfinite(stable_x)) self.volume_data['x'].append(stable_x[non_nans].flatten()) self.volume_data['Q'].append(stable_cap[non_nans].flatten()) self.volume_data['electrode_volume'].append( stable_vol[non_nans].flatten()) self.volume_data['volume_ratio_with_bulk'].append( (stable_vol[non_nans] / stable_vol[0]).flatten()) self.volume_data['volume_expansion_percentage'].append( ((stable_vol[non_nans] / stable_vol[0]) - 1) * 100) self.volume_data['hull_distances'].append( hull_distances[non_nans].flatten()) self.volume_data['endstoichs'].append( stable_stoichs[non_nans].flatten()[0])
def _calculate_binary_voltage_curve(self, hull_cursor): """ Generate binary voltage curve, setting the self.voltage_data dictionary. Parameters: hull_cursor (list(dict)): list of structures to include in the voltage curve. """ mu_enthalpy = get_array_from_cursor(self.chempot_cursor, self.energy_key) # take a copy of the cursor so it can be reordered _hull_cursor = deepcopy(hull_cursor) x = get_num_intercalated(_hull_cursor) _x_order = np.argsort(x) capacities = np.asarray([ get_generic_grav_capacity( get_padded_composition(doc['stoichiometry'], self.elements), self.elements) for doc in _hull_cursor ]) set_cursor_from_array(_hull_cursor, x, 'conc_of_active_ion') set_cursor_from_array(_hull_cursor, capacities, 'gravimetric_capacity') _hull_cursor = sorted(_hull_cursor, key=lambda doc: doc['conc_of_active_ion']) capacities = capacities[_x_order] x = np.sort(x) stable_enthalpy_per_b = get_array_from_cursor( _hull_cursor, self._extensive_energy_key + '_per_b') x, uniq_idxs = np.unique(x, return_index=True) stable_enthalpy_per_b = stable_enthalpy_per_b[uniq_idxs] capacities = capacities[uniq_idxs] V = np.zeros_like(x) V[1:] = -np.diff(stable_enthalpy_per_b) / np.diff(x) + mu_enthalpy[0] V[0] = V[1] reactions = [((None, get_formula_from_stoich(doc['stoichiometry'])), ) for doc in _hull_cursor[1:]] # make V, Q and x available for plotting, stripping NaNs to re-add later # in the edge case of duplicate chemical potentials capacities, unique_caps = np.unique(capacities, return_index=True) non_nan = np.argwhere(np.isfinite(capacities)) V = (np.asarray(V)[unique_caps])[non_nan].flatten().tolist() x = (np.asarray(x)[unique_caps])[non_nan].flatten().tolist() capacities = capacities[non_nan].flatten().tolist() x.append(np.nan) capacities.append(np.nan) V.append(0.0) average_voltage = Electrode.calculate_average_voltage(capacities, V) profile = VoltageProfile( voltages=V, capacities=capacities, average_voltage=average_voltage, starting_stoichiometry=[[self.species[1], 1.0]], reactions=reactions, active_ion=self.species[0]) self.voltage_data.append(profile)
def _calculate_ternary_voltage_curve(self, hull_cursor): """ Calculate tenary voltage curve, setting self.voltage_data. First pass written by James Darby, [email protected]. Parameters: hull_cursor (list(dict)): list of structures to include in the voltage curve. """ # construct working array of concentrations and energies stoichs = get_array_from_cursor(hull_cursor, 'stoichiometry') # do another convex hull on just the known hull points, to allow access to useful indices import scipy.spatial # construct working array of concentrations and energies points = np.hstack(( get_array_from_cursor(hull_cursor, 'concentration'), get_array_from_cursor(hull_cursor, self.energy_key).reshape(len(hull_cursor), 1) )) stoichs = get_array_from_cursor(hull_cursor, 'stoichiometry') # do another convex hull on just the known hull points, to allow access to useful indices convex_hull = scipy.spatial.ConvexHull(points) endpoints, endstoichs = Electrode._find_starting_materials(convex_hull.points, stoichs) print('{} starting point(s) found.'.format(len(endstoichs))) for endstoich in endstoichs: print(get_formula_from_stoich(endstoich), end=' ') print('\n') # iterate over possible delithiated phases for reaction_ind, endpoint in enumerate(endpoints): print(30 * '-') print('Reaction {}, {}:'.format(reaction_ind+1, get_formula_from_stoich(endstoichs[reaction_ind]))) reactions, capacities, voltages, average_voltage, volumes = self._construct_electrode( convex_hull, endpoint, endstoichs[reaction_ind], hull_cursor ) profile = VoltageProfile( starting_stoichiometry=endstoichs[reaction_ind], reactions=reactions, capacities=capacities, voltages=voltages, average_voltage=average_voltage, active_ion=self.species[0] ) self.voltage_data.append(profile) if not self._non_elemental: self.volume_data['x'].append(capacities) self.volume_data['Q'].append(capacities) self.volume_data['electrode_volume'].append(volumes) self.volume_data['endstoichs'].append(endstoichs[reaction_ind]) self.volume_data['volume_ratio_with_bulk'].append(np.asarray(volumes) / volumes[0]) self.volume_data['volume_expansion_percentage'].append(((np.asarray(volumes) / volumes[0]) - 1) * 100) self.volume_data['hull_distances'].append(np.zeros_like(capacities))
def _get_hull_distance(self, generation): """ Assign distance from the hull from hull for generation, assigning it. Parameters: generation (Generation): list of optimised structures. Returns: hull_dist (list(float)): list of distances to the hull. """ for ind, populum in enumerate(generation): generation[ind]["concentration"] = get_concentration( populum, self.hull.elements) generation[ind][ "formation_enthalpy_per_atom"] = get_formation_energy( self.chempots, populum) if self.debug: print( generation[ind]["concentration"], generation[ind]["formation_enthalpy_per_atom"], ) if self.testing: for ind, populum in enumerate(generation): generation[ind][ "formation_enthalpy_per_atom"] = np.random.rand() - 0.5 structures = np.hstack(( get_array_from_cursor(generation, "concentration"), get_array_from_cursor(generation, "formation_enthalpy_per_atom").reshape( len(generation), 1), )) if self.debug: print(structures) hull_dist = self.hull.get_hull_distances(structures, precompute=False) for ind, populum in enumerate(generation): generation[ind]["hull_distance"] = hull_dist[ind] return hull_dist
def _compute_voltages_from_intersections( self, intersections, hull, crossover, endstoich, hull_cursor ): """ Traverse the composition pathway and its intersections with hull facets and compute the voltage and volume drops along the way, for a ternary system with N 3-phase regions. Parameters: intersections (np.ndarray): Nx3 array containing the list of face indices crossed by the path. hull (scipy.spatial.ConvexHull): the temporary hull object that is referred to by any indices. crossover (np.ndarray): (N+1)x3 array containing the coordinates at which the composition pathway crosses the hull faces. endstoich (list): a matador stoichiometry that defines the end of the pathway. hull_cursor (list): the actual structures used to create the hull. """ if len(crossover) != len(intersections) + 1: raise RuntimeError("Incompatible number of crossovers ({}) and intersections ({})." .format(np.shape(crossover), np.shape(intersections))) # set up output arrays voltages = np.empty(len(intersections) + 1) volumes = np.empty(len(intersections)) reactions = [] # set up some useful arrays for later points = hull.points mu_enthalpy = get_array_from_cursor(self.chempot_cursor, self.energy_key) if not self._non_elemental: input_volumes = get_array_from_cursor(hull_cursor, 'cell_volume') / get_array_from_cursor(hull_cursor, 'num_atoms') capacities = [get_generic_grav_capacity(point, self.species) for point in crossover] # initial composition of one formula unit of the starting material initial_comp = get_padded_composition(endstoich, self.species) # loop over intersected faces and compute the voltage and volume at the # crossover with that face for ind, face in enumerate(intersections): simplex_index = int(face[0]) final_stoichs = [hull_cursor[idx]['stoichiometry'] for idx in hull.simplices[simplex_index]] atoms_per_fu = [sum([elem[1] for elem in stoich]) for stoich in final_stoichs] energy_vec = points[hull.simplices[simplex_index], 2] comp = points[hull.simplices[simplex_index], :] comp[:, 2] = 1 - comp[:, 0] - comp[:, 1] # normalize the crossover composition to one formula unit of the starting electrode norm = np.asarray(crossover[ind][1:]) / np.asarray(initial_comp[1:]) ratios_of_phases = np.linalg.solve(comp.T, crossover[ind] / norm[0]) # remove small numerical noise ratios_of_phases[np.where(ratios_of_phases < EPS)] = 0 # create a list containing the sections of the balanced reaction for printing balanced_reaction = [] for i, ratio in enumerate(ratios_of_phases): if ratio < EPS: continue else: formula = get_formula_from_stoich(final_stoichs[i]) balanced_reaction.append((ratio / atoms_per_fu[i], formula)) reactions.append(balanced_reaction) # compute the voltage as the difference between the projection of the gradient down to pure active ion, # and the reference chemical potential comp_inv = np.linalg.inv(comp.T) V = -(comp_inv.dot([1, 0, 0])).dot(energy_vec) V = V + mu_enthalpy[0] # compute the volume of the final electrode if not self._non_elemental: if ind <= len(intersections) - 1: volume_vec = input_volumes[hull.simplices[simplex_index]] volumes[ind] = np.dot(ratios_of_phases, volume_vec) # double up on first voltage if ind == 0: voltages[0] = V voltages[ind+1] = V average_voltage = Electrode.calculate_average_voltage(capacities, voltages) # print the reaction over a few lines, remove 1s and rounded ratios print( ' ---> '.join( [' + '.join( ["{}{}".format( str(round(chem[0], 3)) + ' ' if abs(chem[0] - 1) > EPS else '', chem[1]) for chem in region]) for region in reactions]) ) return reactions, capacities, voltages, average_voltage, volumes
def __init__(self, cursor, formation_key, dimension): """ Compute the convex hull of data passed, to retrieve hull distances and thus stable structures. Parameters: cursor (list[dict]): list of matador documents to make phase diagram from. formation_key (str or list): location of the formation energy inside each document, either a single key or iterable of keys to use with `recursive_get`. """ self._dimension = dimension self.cursor = cursor self.formation_key = formation_key structures = np.hstack(( get_array_from_cursor(cursor, 'concentration').reshape(len(cursor), dimension-1), get_array_from_cursor(cursor, self.formation_key).reshape(len(cursor), 1))) # define self._structure_slice as the filtered array of points actually used to create the convex hull # which can include/exclude points from the passed structures. This array is the one indexed by # vertices/simplices in ConvexHull if self._dimension == 3: # add a point "above" the hull # for simple removal of extraneous vertices (e.g. top of 2D hull) dummy_point = [0.333, 0.333, 1e5] # if ternary, use all structures, not just those with negative eform for compatibility reasons self._structure_slice = np.vstack((structures, dummy_point)) else: # filter out those with positive formation energy, to reduce expense computing hull self._structure_slice = structures[np.where(structures[:, -1] <= 0 + EPS)] # filter out "duplicates" in _structure_slice # this prevents breakages if no structures are on the hull and chempots are duplicated # but it might be faster to hardcode this case individually self._structure_slice = np.unique(self._structure_slice, axis=0) # if we only have the chempots (or worse) with negative formation energy, don't even make the hull if len(self._structure_slice) <= dimension: if len(self._structure_slice) < dimension: raise RuntimeError('No chemical potentials on hull... either mysterious use of custom chempots, or worry!') self.convex_hull = FakeHull() else: try: self.convex_hull = scipy.spatial.ConvexHull(self._structure_slice) except scipy.spatial.qhull.QhullError: print(self._structure_slice) print('Error with QHull, plotting formation energies only...') print_exc() self.convex_hull = FakeHull() # remove vertices that have positive formation energy filtered_vertices = [vertex for vertex in self.convex_hull.vertices if self._structure_slice[vertex, -1] <= 0 + EPS] bad_simplices = set() for ind, simplex in enumerate(self.convex_hull.simplices): for vertex in simplex: if vertex not in filtered_vertices: bad_simplices.add(ind) filtered_simplices = [simplex for ind, simplex in enumerate(self.convex_hull.simplices) if ind not in bad_simplices] self.convex_hull = FakeHull() self.convex_hull.points = self._structure_slice self.convex_hull.vertices = list(filtered_vertices) self.convex_hull.simplices = list(filtered_simplices) self.hull_dist = self.get_hull_distances(structures, precompute=True) set_cursor_from_array(self.cursor, self.hull_dist, 'hull_distance') self.structures = structures self.stable_structures = [doc for doc in self.cursor if doc['hull_distance'] < EPS]