def test_pbc_shortest_vectors(self): fcoords = np.array([[0.3, 0.3, 0.5], [0.1, 0.1, 0.3], [0.9, 0.9, 0.8], [0.1, 0.0, 0.5], [0.9, 0.7, 0.0]]) lattice = Lattice.from_lengths_and_angles([8, 8, 4], [90, 76, 58]) expected = np.array([[0.000, 3.015, 4.072, 3.519, 3.245], [3.015, 0.000, 3.207, 1.131, 4.453], [4.072, 3.207, 0.000, 2.251, 1.788], [3.519, 1.131, 2.251, 0.000, 3.852]]) vectors = pbc_shortest_vectors(lattice, fcoords[:-1], fcoords) dists = np.sum(vectors**2, axis = -1)**0.5 self.assertArrayAlmostEqual(dists, expected, 3) #now try with small loop threshold from pymatgen.util import coord_utils prev_threshold = coord_utils.LOOP_THRESHOLD coord_utils.LOOP_THRESHOLD = 0 vectors = pbc_shortest_vectors(lattice, fcoords[:-1], fcoords) dists = np.sum(vectors**2, axis = -1)**0.5 self.assertArrayAlmostEqual(dists, expected, 3) coord_utils.LOOP_THRESHOLD = prev_threshold
def _cart_dists(self, s1, s2, avg_lattice, mask, normalization): """ Finds a matching in cartesian space. Finds an additional fractional translation vector to minimize RMS distance Args: s1, s2: numpy arrays of fractional coordinates. len(s1) >= len(s2) avg_lattice: Lattice on which to calculate distances mask: numpy array of booleans. mask[i, j] = True indicates that s2[i] cannot be matched to s1[j] normalization (float): inverse normalization length Returns: Distances from s2 to s1, normalized by (V/Natom) ^ 1/3 Fractional translation vector to apply to s2. Mapping from s1 to s2, i.e. with numpy slicing, s1[mapping] => s2 """ if len(s2) > len(s1): raise ValueError("s1 must be larger than s2") if mask.shape != (len(s2), len(s1)): raise ValueError("mask has incorrect shape") mask_val = 1e10 * self.stol / normalization #vectors are from s2 to s1 vecs = pbc_shortest_vectors(avg_lattice, s2, s1) vecs[mask] = mask_val d_2 = np.sum(vecs**2, axis=-1) lin = LinearAssignment(d_2) s = lin.solution short_vecs = vecs[np.arange(len(s)), s] translation = np.average(short_vecs, axis=0) f_translation = avg_lattice.get_fractional_coords(translation) new_d2 = np.sum((short_vecs - translation)**2, axis=-1) return new_d2**0.5 * normalization, f_translation, s
def get_distance_and_image(self, frac_coords1, frac_coords2, jimage=None): """ Gets distance between two frac_coords assuming periodic boundary conditions. If the index jimage is not specified it selects the j image nearest to the i atom and returns the distance and jimage indices in terms of lattice vector translations. If the index jimage is specified it returns the distance between the frac_coords1 and the specified jimage of frac_coords2, and the given jimage is also returned. Args: fcoords1 (3x1 array): Reference fcoords to get distance from. fcoords2 (3x1 array): fcoords to get distance from. jimage (3x1 array): Specific periodic image in terms of lattice translations, e.g., [1,0,0] implies to take periodic image that is one a-lattice vector away. If jimage is None, the image that is nearest to the site is found. Returns: (distance, jimage): distance and periodic lattice translations of the other site for which the distance applies. This means that the distance between frac_coords1 and (jimage + frac_coords2) is equal to distance. """ if jimage is None: v, d2 = pbc_shortest_vectors(self, frac_coords1, frac_coords2, return_d2=True) fc = self.get_fractional_coords(v[0][0]) + frac_coords1 - \ frac_coords2 fc = np.array(np.round(fc), dtype=np.int) return (np.sqrt(d2[0, 0]), fc) mapped_vec = self.get_cartesian_coords(jimage + frac_coords2 - frac_coords1) return np.linalg.norm(mapped_vec), jimage
def _cart_dists(self, s1, s2, avg_lattice, mask, normalization): """ Finds a matching in cartesian space. Finds an additional fractional translation vector to minimize RMS distance Args: s1, s2: numpy arrays of fractional coordinates. len(s1) >= len(s2) avg_lattice: Lattice on which to calculate distances mask: numpy array of booleans. mask[i, j] = True indicates that s2[i] cannot be matched to s1[j] normalization (float): inverse normalization length Returns: Distances from s2 to s1, normalized by (V/Natom) ^ 1/3 Fractional translation vector to apply to s2. Mapping from s1 to s2, i.e. with numpy slicing, s1[mapping] => s2 """ if len(s2) > len(s1): raise ValueError("s1 must be larger than s2") if mask.shape != (len(s2), len(s1)): raise ValueError("mask has incorrect shape") mask_val = 1e10 * self.stol / normalization #vectors are from s2 to s1 vecs = pbc_shortest_vectors(avg_lattice, s2, s1) vecs[mask] = mask_val d_2 = np.sum(vecs ** 2, axis=-1) lin = LinearAssignment(d_2) s = lin.solution short_vecs = vecs[np.arange(len(s)), s] translation = np.average(short_vecs, axis=0) f_translation = avg_lattice.get_fractional_coords(translation) new_d2 = np.sum((short_vecs - translation) ** 2, axis=-1) return new_d2 ** 0.5 * normalization, f_translation, s
def get_distance_and_image(self, frac_coords1, frac_coords2, jimage=None): """ Gets distance between two frac_coords assuming periodic boundary conditions. If the index jimage is not specified it selects the j image nearest to the i atom and returns the distance and jimage indices in terms of lattice vector translations. If the index jimage is specified it returns the distance between the frac_coords1 and the specified jimage of frac_coords2, and the given jimage is also returned. Args: fcoords1 (3x1 array): Reference fcoords to get distance from. fcoords2 (3x1 array): fcoords to get distance from. jimage (3x1 array): Specific periodic image in terms of lattice translations, e.g., [1,0,0] implies to take periodic image that is one a-lattice vector away. If jimage == None, the image that is nearest to the site is found. Returns: (distance, jimage): distance and periodic lattice translations of the other site for which the distance applies. This means that the distance between frac_coords1 and (jimage + frac_coords2) is equal to distance. """ if jimage is None: v, d2 = pbc_shortest_vectors(self, frac_coords1, frac_coords2, return_d2=True) fc = self.get_fractional_coords(v[0][0]) + frac_coords1 - \ frac_coords2 fc = np.array(np.round(fc), dtype=np.int) return (np.sqrt(d2[0, 0]), fc) mapped_vec = self.get_cartesian_coords(jimage + frac_coords2 - frac_coords1) return np.linalg.norm(mapped_vec), jimage
def _cart_dists(self, s1, s2, l1, l2, mask): """ Finds the cartesian distances normalized by (V/Natom) ^ 1/3 between s1 and s2 on the average lattice of l1 and l2 s1 and s2 are lists of fractional coordinates. Minimizes the RMS distance of the matching with an additional translation (but doesn't change the mapping) returns distances, fractional_translation vector """ #create the average lattice avg_params = (np.array(l1.lengths_and_angles) + np.array(l2.lengths_and_angles)) / 2 avg_lattice = Lattice.from_lengths_and_angles(*avg_params) norm_length = (avg_lattice.volume / len(s1)) ** (1 / 3) mask_val = 1e20 * norm_length * self.stol all_d_2 = np.zeros([len(s1), len(s1)]) vec_matrix = np.zeros([len(s1), len(s1), 3]) vecs = pbc_shortest_vectors(avg_lattice, s2, s1) vec_matrix[:len(s2)] = vecs vec_matrix[mask] = mask_val d_2 = (np.sum(vecs ** 2, axis=-1)) all_d_2[:len(s2)] = d_2 all_d_2[mask] = mask_val lin = LinearAssignment(all_d_2) inds = np.arange(len(s2)) shortest_vecs = vec_matrix[inds, lin.solution[:len(s2)], :] translation = np.average(shortest_vecs, axis=0) f_translation = avg_lattice.get_fractional_coords(translation) shortest_distances = np.sum((shortest_vecs - translation) ** 2, -1) ** 0.5 return shortest_distances / norm_length, f_translation
def test_pbc_shortest_vectors(self): fcoords = np.array([[0.3, 0.3, 0.5], [0.1, 0.1, 0.3], [0.9, 0.9, 0.8], [0.1, 0.0, 0.5], [0.9, 0.7, 0.0]]) lattice = Lattice.from_lengths_and_angles([8, 8, 4], [90, 76, 58]) expected = np.array([[0.000, 3.015, 4.072, 3.519, 3.245], [3.015, 0.000, 3.207, 1.131, 4.453], [4.072, 3.207, 0.000, 2.251, 1.788], [3.519, 1.131, 2.251, 0.000, 3.852]]) vectors = pbc_shortest_vectors(lattice, fcoords[:-1], fcoords) dists = np.sum(vectors**2, axis=-1)**0.5 self.assertArrayAlmostEqual(dists, expected, 3)
def test_pbc_shortest_vectors(self): fcoords = np.array([[0.3, 0.3, 0.5], [0.1, 0.1, 0.3], [0.9, 0.9, 0.8], [0.1, 0.0, 0.5], [0.9, 0.7, 0.0]]) lattice = Lattice.from_lengths_and_angles([8, 8, 4], [90, 76, 58]) expected = np.array([[0.000, 3.015, 4.072, 3.519, 3.245], [3.015, 0.000, 3.207, 1.131, 4.453], [4.072, 3.207, 0.000, 2.251, 1.788], [3.519, 1.131, 2.251, 0.000, 3.852]]) vectors = pbc_shortest_vectors(lattice, fcoords[:-1], fcoords) dists = np.sum(vectors**2, axis = -1)**0.5 self.assertArrayAlmostEqual(dists, expected, 3)
def _cart_dists(self, s1, s2, l1, l2, mask): """ Finds the cartesian distances normalized by (V/Natom) ^ 1/3 between two structures on the average lattice of l1 and l2 s_superset and s_subset are lists of fractional coordinates. Minimizes the RMS distance of the matching with an additional translation (but doesn't change the mapping) returns distances, fractional_translation vector """ #ensure that we always calculate distances from the subset #to the superset if len(s1) > len(s2): s_superset, s_subset, mult = s1, s2, 1 else: s_superset, s_subset, mult = s2, s1, -1 mask = mask.T #create the average lattice avg_params = (np.array(l1.lengths_and_angles) + np.array(l2.lengths_and_angles)) / 2 avg_lattice = Lattice.from_lengths_and_angles(*avg_params) norm_length = (avg_lattice.volume / len(s_superset)) ** (1 / 3) mask_val = 1e20 * norm_length * self.stol all_d_2 = np.zeros([len(s_superset), len(s_superset)]) vec_matrix = np.zeros([len(s_superset), len(s_superset), 3]) #vectors from subset to superset #1st index subset, 2nd index superset vecs = pbc_shortest_vectors(avg_lattice, s_subset, s_superset) vec_matrix[:len(s_subset), :len(s_superset)] = vecs vec_matrix[mask] = mask_val d_2 = (np.sum(vecs ** 2, axis=-1)) all_d_2[:len(s_subset), :len(s_superset)] = d_2 all_d_2[mask] = mask_val lin = LinearAssignment(all_d_2) inds = np.arange(len(s_subset)) #shortest vectors from the subset to the superset shortest_vecs = vec_matrix[inds, lin.solution[:len(s_subset)], :] translation = np.average(shortest_vecs, axis=0) f_translation = avg_lattice.get_fractional_coords(translation) shortest_distances = np.sum((shortest_vecs - translation) ** 2, -1) ** 0.5 return shortest_distances / norm_length, f_translation * mult
def _cmp_cartesian_struct(self, s1, s2, l1, l2): """ Once a fit is found, a rms minimizing fit is done to ensure the fit is correct. To do this, 1) The structures are placed into an average lattice 2) All sites are shifted by the mean displacement vector between matched sites. 3) calculate distances 4) return rms distance normalized by (V/Natom) ^ 1/3 and the maximum distance found """ nsites = sum(map(len, s1)) avg_params = (np.array(l1.lengths_and_angles) + np.array(l2.lengths_and_angles)) / 2 avg_lattice = Lattice.from_lengths_and_angles(avg_params[0], avg_params[1]) dist = np.zeros([nsites, nsites]) + 100 * nsites vec_matrix = np.zeros([nsites, nsites, 3]) i = 0 for s1_coords, s2_coords in zip(s1, s2): j = len(s1_coords) vecs = pbc_shortest_vectors(avg_lattice, s1_coords, s2_coords) distances = (np.sum(vecs ** 2, axis=-1)) ** 0.5 dist[i: i + j, i: i + j] = distances vec_matrix[i: i + j, i: i + j] = vecs i += j lin = LinearAssignment(dist) inds = np.arange(nsites) shortest_vecs = vec_matrix[inds, lin.solution, :] shortest_vec_square = np.sum( (shortest_vecs - np.average(shortest_vecs, axis=0)) ** 2, -1) norm_length = (avg_lattice.volume / nsites) ** (1 / 3) rms = np.average(shortest_vec_square) ** 0.5 / norm_length max_dist = np.max(shortest_vec_square) ** 0.5 / norm_length return rms, max_dist
def _cmp_cartesian_struct(self, s1, s2, l1, l2): """ Once a fit is found, a rms minimizing fit is done to ensure the fit is correct. To do this, 1) The structures are placed into an average lattice 2) All sites are shifted by the mean displacement vector between matched sites. 3) calculate distances 4) return rms distance normalized by (V/Natom) ^ 1/3 and the maximum distance found """ nsites = sum(map(len, s1)) avg_params = (np.array(l1.lengths_and_angles) + np.array(l2.lengths_and_angles)) / 2 avg_lattice = Lattice.from_lengths_and_angles(avg_params[0], avg_params[1]) dist = np.zeros([nsites, nsites]) + 100 * nsites vec_matrix = np.zeros([nsites, nsites, 3]) i = 0 for s1_coords, s2_coords in zip(s1, s2): j = len(s1_coords) vecs = pbc_shortest_vectors(avg_lattice, s1_coords, s2_coords) distances = (np.sum(vecs**2, axis=-1))**0.5 dist[i:i + j, i:i + j] = distances vec_matrix[i:i + j, i:i + j] = vecs i += j lin = LinearAssignment(dist) inds = np.arange(nsites) shortest_vecs = vec_matrix[inds, lin.solution, :] shortest_vec_square = np.sum( (shortest_vecs - np.average(shortest_vecs, axis=0))**2, -1) norm_length = (avg_lattice.volume / nsites)**(1 / 3) rms = np.average(shortest_vec_square)**0.5 / norm_length max_dist = np.max(shortest_vec_square)**0.5 / norm_length return rms, max_dist
def _cart_dists(self, s1, s2, l1, l2, mask): """ Finds the cartesian distances normalized by (V/Natom) ^ 1/3 between two structures on the average lattice of l1 and l2 s_superset and s_subset are lists of fractional coordinates. Minimizes the RMS distance of the matching with an additional translation (but doesn't change the mapping) returns distances, fractional_translation vector """ #ensure that we always calculate distances from the subset #to the superset if len(s1) > len(s2): s_superset, s_subset, mult = s1, s2, 1 else: s_superset, s_subset, mult = s2, s1, -1 mask = mask.T #create the average lattice avg_params = (np.array(l1.lengths_and_angles) + np.array(l2.lengths_and_angles)) / 2 avg_lattice = Lattice.from_lengths_and_angles(*avg_params) norm_length = (avg_lattice.volume / len(s_superset))**(1 / 3) mask_val = 1e20 * norm_length * self.stol all_d_2 = np.zeros([len(s_superset), len(s_superset)]) vec_matrix = np.zeros([len(s_superset), len(s_superset), 3]) #vectors from subset to superset #1st index subset, 2nd index superset vecs = pbc_shortest_vectors(avg_lattice, s_subset, s_superset) vec_matrix[:len(s_subset), :len(s_superset)] = vecs vec_matrix[mask] = mask_val d_2 = (np.sum(vecs**2, axis=-1)) all_d_2[:len(s_subset), :len(s_superset)] = d_2 all_d_2[mask] = mask_val lin = LinearAssignment(all_d_2) inds = np.arange(len(s_subset)) #shortest vectors from the subset to the superset shortest_vecs = vec_matrix[inds, lin.solution[:len(s_subset)], :] translation = np.average(shortest_vecs, axis=0) f_translation = avg_lattice.get_fractional_coords(translation) shortest_distances = np.sum((shortest_vecs - translation)**2, -1)**0.5 return shortest_distances / norm_length, f_translation * mult
def get_all_distances(self, fcoords1, fcoords2): """ Returns the distances between two lists of coordinates taking into account periodic boundary conditions and the lattice. Note that this computes an MxN array of distances (i.e. the distance between each point in fcoords1 and every coordinate in fcoords2). This is different functionality from pbc_diff. Args: fcoords1: First set of fractional coordinates. e.g., [0.5, 0.6, 0.7] or [[1.1, 1.2, 4.3], [0.5, 0.6, 0.7]]. It can be a single coord or any array of coords. fcoords2: Second set of fractional coordinates. Returns: 2d array of cartesian distances. E.g the distance between fcoords1[i] and fcoords2[j] is distances[i,j] """ v, d2 = pbc_shortest_vectors(self, fcoords1, fcoords2, return_d2=True) return np.sqrt(d2)