def __init__( self, lattice_mat=None, coords=None, elements=None, props=None, cartesian=False, show_props=False, ): """ Create atomic structure. Requires lattice, coordinates, atom type information. >>> box = [[2.715, 2.715, 0], [0, 2.715, 2.715], [2.715, 0, 2.715]] >>> coords = [[0, 0, 0], [0.25, 0.2, 0.25]] >>> elements = ["Si", "Si"] >>> Si = Atoms(lattice_mat=box, coords=coords, elements=elements) >>> print(round(Si.volume,2)) 40.03 >>> Si.composition {'Si': 2} >>> round(Si.density,2) 2.33 >>> round(Si.packing_fraction,2) 0.28 >>> Si.atomic_numbers [14, 14] >>> Si.num_atoms 2 >>> Si.frac_coords[0][0] 0 >>> Si.cart_coords[0][0] 0.0 >>> coords = [[0, 0, 0], [1.3575 , 1.22175, 1.22175]] >>> round(Si.density,2) 2.33 >>> Si.spacegroup() 'C2/m (12)' >>> Si.pymatgen_converter()!={} True """ self.lattice_mat = np.array(lattice_mat) self.show_props = show_props self.lattice = Lattice(lattice_mat) self.coords = np.array(coords) self.elements = elements self.cartesian = cartesian self.props = props if self.props is None: self.props = ["" for i in range(len(self.elements))] if self.cartesian: self.cart_coords = self.coords self.frac_coords = np.array(self.lattice.frac_coords(self.coords)) else: self.frac_coords = self.coords self.cart_coords = np.array(self.lattice.cart_coords(self.coords))
def make_supercell_matrix(self, scaling_matrix): """ Adapted from Pymatgen. Makes a supercell. Allowing to have sites outside the unit cell. Args: scaling_matrix: A scaling matrix for transforming the lattice vectors. Has to be all integers. Several options are possible: a. A full 3x3 scaling matrix defining the linear combination the old lattice vectors. E.g., [[2,1,0],[0,3,0],[0,0, 1]] generates a new structure with lattice vectors a' = 2a + b, b' = 3b, c' = c where a, b, and c are the lattice vectors of the original structure. b. An sequence of three scaling factors. E.g., [2, 1, 1] specifies that the supercell should have dimensions 2a x b x c. c. A number, which simply scales all lattice vectors by the same factor. Returns: Supercell structure. Note that a Structure is always returned, even if the input structure is a subclass of Structure. This is to avoid different arguments signatures from causing problems. If you prefer a subclass to return its own type, you need to override this method in the subclass. """ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) new_lattice = Lattice(np.dot(scale_matrix, self.lattice_mat)) f_lat = self.lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.cart_coords(f_lat) new_sites = [] new_elements = [] for site, el in zip(self.cart_coords, self.elements): for v in c_lat: new_elements.append(el) tmp = site + v new_sites.append(tmp) return Atoms( lattice_mat=new_lattice.lattice(), elements=new_elements, coords=new_sites, cartesian=True, )
def automatic_length_mesh(self, lattice_mat=[], length=20, header="Gamma"): """Length based automatic k-points.""" inv_lat = Lattice(lattice_mat=lattice_mat).inv_lattice() b1 = LA.norm(np.array(inv_lat[0])) b2 = LA.norm(np.array(inv_lat[1])) b3 = LA.norm(np.array(inv_lat[2])) n1 = int(max(1, length * b1 + 0.5)) n2 = int(max(1, length * b2 + 0.5)) n3 = int(max(1, length * b3 + 0.5)) return Kpoints3D(kpoints=[[n1, n2, n3]], header=header, kpoint_mode="automatic")
def is_all_acute_or_obtuse(m): recp_angles = np.array(Lattice(m).reciprocal_lattice().angles) return np.all(recp_angles <= 90) or np.all(recp_angles > 90)
def conventional_standard_structure(self, tol=1e-5, international_monoclinic=True): """ Give a conventional cell according to certain conventions. The conventionss are defined in Setyawan, W., & Curtarolo, S. (2010). High-throughput electronic band structure calculations: Challenges and tools. Computational Materials Science, 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010 They basically enforce as much as possible norm(a1)<norm(a2)<norm(a3) Returns: The structure in a conventional standardized cell """ struct = self.refined_atoms latt = struct.lattice latt_type = self.lattice_system sorted_lengths = sorted(latt.abc) sorted_dic = sorted( [{ "vec": latt.matrix[i], "length": latt.abc[i], "orig_index": i } for i in [0, 1, 2]], key=lambda k: k["length"], ) if latt_type in ("orthorhombic", "cubic"): # you want to keep the c axis where it is # to keep the C- settings transf = np.zeros(shape=(3, 3)) if self.space_group_symbol.startswith("C"): transf[2] = [0, 0, 1] a, b = sorted(latt.abc[:2]) sorted_dic = sorted( [{ "vec": latt.matrix[i], "length": latt.abc[i], "orig_index": i } for i in [0, 1]], key=lambda k: k["length"], ) for i in range(2): transf[i][sorted_dic[i]["orig_index"]] = 1 c = latt.abc[2] elif self.space_group_symbol.startswith( "A" ): # change to C-centering to match Setyawan/Curtarolo convention transf[2] = [1, 0, 0] a, b = sorted(latt.abc[1:]) sorted_dic = sorted( [{ "vec": latt.matrix[i], "length": latt.abc[i], "orig_index": i } for i in [1, 2]], key=lambda k: k["length"], ) for i in range(2): transf[i][sorted_dic[i]["orig_index"]] = 1 c = latt.abc[0] else: for i in range(len(sorted_dic)): transf[i][sorted_dic[i]["orig_index"]] = 1 a, b, c = sorted_lengths latt = Lattice.orthorhombic(a, b, c) elif latt_type == "tetragonal": # find the "a" vectors # it is basically the vector repeated two times transf = np.zeros(shape=(3, 3)) a, b, c = sorted_lengths for d in range(len(sorted_dic)): transf[d][sorted_dic[d]["orig_index"]] = 1 if abs(b - c) < tol and abs(a - c) > tol: a, c = c, a transf = np.dot([[0, 0, 1], [0, 1, 0], [1, 0, 0]], transf) latt = Lattice.tetragonal(a, c) elif latt_type in ("hexagonal", "rhombohedral"): # for the conventional cell representation, # we allways show the rhombohedral lattices as hexagonal # check first if we have the refined structure shows a rhombohedral # cell # if so, make a supercell a, b, c = latt.abc if np.all(np.abs([a - b, c - b, a - c]) < 0.001): struct.make_supercell(((1, -1, 0), (0, 1, -1), (1, 1, 1))) a, b, c = sorted(struct.lattice.abc) if abs(b - c) < 0.001: a, c = c, a new_matrix = [ [a / 2, -a * np.sqrt(3) / 2, 0], [a / 2, a * np.sqrt(3) / 2, 0], [0, 0, c], ] latt = Lattice(new_matrix) transf = np.eye(3, 3) elif latt_type == "monoclinic": # You want to keep the c axis where it is to keep the C- settings if self.space_group_symbol.startswith("C"): transf = np.zeros(shape=(3, 3)) transf[2] = [0, 0, 1] sorted_dic = sorted( [{ "vec": latt.matrix[i], "length": latt.abc[i], "orig_index": i } for i in [0, 1]], key=lambda k: k["length"], ) a = sorted_dic[0]["length"] b = sorted_dic[1]["length"] c = latt.abc[2] new_matrix = None for t in itertools.permutations(list(range(2)), 2): m = latt.matrix latt2 = Lattice([m[t[0]], m[t[1]], m[2]]) lengths = latt2.abc angles = latt2.angles if angles[0] > 90: # if the angle is > 90 we invert a and b to get # an angle < 90 a, b, c, alpha, beta, gamma = Lattice( [-m[t[0]], -m[t[1]], m[2]]).parameters transf = np.zeros(shape=(3, 3)) transf[0][t[0]] = -1 transf[1][t[1]] = -1 transf[2][2] = 1 alpha = np.pi * alpha / 180 new_matrix = [ [a, 0, 0], [0, b, 0], [0, c * cos(alpha), c * sin(alpha)], ] continue elif angles[0] < 90: transf = np.zeros(shape=(3, 3)) # print ('464-470') transf[0][t[0]] = 1 transf[1][t[1]] = 1 transf[2][2] = 1 a, b, c = lengths alpha = np.pi * angles[0] / 180 new_matrix = [ [a, 0, 0], [0, b, 0], [0, c * cos(alpha), c * sin(alpha)], ] if new_matrix is None: # print ('479-482') # this if is to treat the case # where alpha==90 (but we still have a monoclinic sg new_matrix = [[a, 0, 0], [0, b, 0], [0, 0, c]] transf = np.zeros(shape=(3, 3)) for c in range(len(sorted_dic)): transf[c][sorted_dic[c]["orig_index"]] = 1 # if not C-setting else: # try all permutations of the axis # keep the ones with the non-90 angle=alpha # and b<c new_matrix = None for t in itertools.permutations(list(range(3)), 3): m = latt.matrix a, b, c, alpha, beta, gamma = Lattice( [m[t[0]], m[t[1]], m[t[2]]]).parameters if alpha > 90 and b < c: a, b, c, alpha, beta, gamma = Lattice( [-m[t[0]], -m[t[1]], m[t[2]]]).parameters transf = np.zeros(shape=(3, 3)) transf[0][t[0]] = -1 transf[1][t[1]] = -1 transf[2][t[2]] = 1 alpha = np.pi * alpha / 180 new_matrix = [ [a, 0, 0], [0, b, 0], [0, c * cos(alpha), c * sin(alpha)], ] continue elif alpha < 90 and b < c: # print ('510-515') transf = np.zeros(shape=(3, 3)) transf[0][t[0]] = 1 transf[1][t[1]] = 1 transf[2][t[2]] = 1 alpha = np.pi * alpha / 180 new_matrix = [ [a, 0, 0], [0, b, 0], [0, c * cos(alpha), c * sin(alpha)], ] if new_matrix is None: # print ('523-530') # this if is to treat the case # where alpha==90 (but we still have a monoclinic sg new_matrix = [ [sorted_lengths[0], 0, 0], [0, sorted_lengths[1], 0], [0, 0, sorted_lengths[2]], ] transf = np.zeros(shape=(3, 3)) for c in range(len(sorted_dic)): transf[c][sorted_dic[c]["orig_index"]] = 1 if international_monoclinic: # The above code makes alpha the non-right angle. # The following will convert to proper international convention # that beta is the non-right angle. op = [[0, 1, 0], [1, 0, 0], [0, 0, -1]] transf = np.dot(op, transf) new_matrix = np.dot(op, new_matrix) beta = Lattice(new_matrix).beta if beta < 90: op = [[-1, 0, 0], [0, -1, 0], [0, 0, 1]] transf = np.dot(op, transf) new_matrix = np.dot(op, new_matrix) latt = Lattice(new_matrix) elif latt_type == "triclinic": # we use a LLL Minkowski-like reduction for the triclinic cells struct = struct.get_lll_reduced_structure() a, b, c = latt.abc # lengths alpha, beta, gamma = [np.pi * i / 180 for i in latt.angles] new_matrix = None test_matrix = [ [a, 0, 0], [b * cos(gamma), b * sin(gamma), 0.0], [ c * cos(beta), c * (cos(alpha) - cos(beta) * cos(gamma)) / sin(gamma), c * np.sqrt( sin(gamma)**2 - cos(alpha)**2 - cos(beta)**2 + 2 * cos(alpha) * cos(beta) * cos(gamma)) / sin(gamma), ], ] def is_all_acute_or_obtuse(m): recp_angles = np.array(Lattice(m).reciprocal_lattice().angles) return np.all(recp_angles <= 90) or np.all(recp_angles > 90) if is_all_acute_or_obtuse(test_matrix): transf = np.eye(3) new_matrix = test_matrix test_matrix = [ [-a, 0, 0], [b * cos(gamma), b * sin(gamma), 0.0], [ -c * cos(beta), -c * (cos(alpha) - cos(beta) * cos(gamma)) / sin(gamma), -c * np.sqrt( sin(gamma)**2 - cos(alpha)**2 - cos(beta)**2 + 2 * cos(alpha) * cos(beta) * cos(gamma)) / sin(gamma), ], ] if is_all_acute_or_obtuse(test_matrix): transf = [[-1, 0, 0], [0, 1, 0], [0, 0, -1]] new_matrix = test_matrix test_matrix = [ [-a, 0, 0], [-b * cos(gamma), -b * sin(gamma), 0.0], [ c * cos(beta), c * (cos(alpha) - cos(beta) * cos(gamma)) / sin(gamma), c * np.sqrt( sin(gamma)**2 - cos(alpha)**2 - cos(beta)**2 + 2 * cos(alpha) * cos(beta) * cos(gamma)) / sin(gamma), ], ] if is_all_acute_or_obtuse(test_matrix): transf = [[-1, 0, 0], [0, -1, 0], [0, 0, 1]] new_matrix = test_matrix test_matrix = [ [a, 0, 0], [-b * cos(gamma), -b * sin(gamma), 0.0], [ -c * cos(beta), -c * (cos(alpha) - cos(beta) * cos(gamma)) / sin(gamma), -c * np.sqrt( sin(gamma)**2 - cos(alpha)**2 - cos(beta)**2 + 2 * cos(alpha) * cos(beta) * cos(gamma)) / sin(gamma), ], ] if is_all_acute_or_obtuse(test_matrix): transf = [[1, 0, 0], [0, -1, 0], [0, 0, -1]] new_matrix = test_matrix latt = Lattice(new_matrix) new_coords = np.dot(transf, np.transpose(struct.frac_coords)).T new_struct = Atoms( lattice_mat=latt.matrix, elements=struct.elements, coords=new_coords, cartesian=False, ) return new_struct
def make_interface( film="", subs="", atol=1, ltol=0.05, max_area=500, max_area_ratio_tol=1.00, seperation=3.0, vacuum=8.0, apply_strain=False, ): """ Use as main function for making interfaces/heterostructures. Return mismatch and other information as info dict. Args: film: top/film material. subs: substrate/bottom/fixed material. seperation: minimum seperation between two. vacuum: vacuum will be added on both sides. So 2*vacuum will be added. """ z = ZSLGenerator( max_area_ratio_tol=max_area_ratio_tol, max_area=max_area, max_length_tol=ltol, max_angle_tol=atol, ) film = fix_pbc(film.center_around_origin([0, 0, 0])) subs = fix_pbc(subs.center_around_origin([0, 0, 0])) matches = list(z(film.lattice_mat[:2], subs.lattice_mat[:2], lowest=True)) info = {} info["mismatch_u"] = "na" info["mismatch_v"] = "na" info["mismatch_angle"] = "na" info["area1"] = "na" info["area2"] = "na" info["film_sl"] = "na" info["matches"] = matches info["subs_sl"] = "na" uv1 = matches[0]["sub_sl_vecs"] uv2 = matches[0]["film_sl_vecs"] u = np.array(uv1) v = np.array(uv2) a1 = u[0] a2 = u[1] b1 = v[0] b2 = v[1] mismatch_u = np.linalg.norm(b1) / np.linalg.norm(a1) - 1 mismatch_v = np.linalg.norm(b2) / np.linalg.norm(a2) - 1 angle1 = ( np.arccos(np.dot(a1, a2) / np.linalg.norm(a1) / np.linalg.norm(a2)) * 180 / np.pi ) angle2 = ( np.arccos(np.dot(b1, b2) / np.linalg.norm(b1) / np.linalg.norm(b2)) * 180 / np.pi ) mismatch_angle = abs(angle1 - angle2) area1 = np.linalg.norm(np.cross(a1, a2)) area2 = np.linalg.norm(np.cross(b1, b2)) uv_substrate = uv1 uv_film = uv2 substrate_latt = Lattice( np.array( [uv_substrate[0][:], uv_substrate[1][:], subs.lattice_mat[2, :]] ) ) _, __, scell = subs.lattice.find_matches( substrate_latt, ltol=ltol, atol=atol ) film_latt = Lattice( np.array([uv_film[0][:], uv_film[1][:], film.lattice_mat[2, :]]) ) scell[2] = np.array([0, 0, 1]) scell_subs = scell _, __, scell = film.lattice.find_matches(film_latt, ltol=ltol, atol=atol) scell[2] = np.array([0, 0, 1]) scell_film = scell film_scell = film.make_supercell_matrix(scell_film) subs_scell = subs.make_supercell_matrix(scell_subs) info["mismatch_u"] = mismatch_u info["mismatch_v"] = mismatch_v print("mismatch_u,mismatch_v", mismatch_u, mismatch_v) info["mismatch_angle"] = mismatch_angle info["area1"] = area1 info["area2"] = area2 info["film_sl"] = film_scell info["subs_sl"] = subs_scell substrate_top_z = max(np.array(subs_scell.cart_coords)[:, 2]) substrate_bot_z = min(np.array(subs_scell.cart_coords)[:, 2]) film_top_z = max(np.array(film_scell.cart_coords)[:, 2]) film_bottom_z = min(np.array(film_scell.cart_coords)[:, 2]) thickness_sub = abs(substrate_top_z - substrate_bot_z) thickness_film = abs(film_top_z - film_bottom_z) sub_z = ( (vacuum + substrate_top_z) * np.array(subs_scell.lattice_mat[2, :]) / np.linalg.norm(subs_scell.lattice_mat[2, :]) ) shift_normal = ( sub_z / np.linalg.norm(sub_z) * seperation / np.linalg.norm(sub_z) ) tmp = ( thickness_film / 2 + seperation + thickness_sub / 2 ) / np.linalg.norm(subs_scell.lattice_mat[2, :]) shift_normal = ( tmp * np.array(subs_scell.lattice_mat[2, :]) / np.linalg.norm(subs_scell.lattice_mat[2, :]) ) interface = add_atoms( film_scell, subs_scell, shift_normal, apply_strain=apply_strain ).center_around_origin([0, 0, 0.5]) combined = interface.center(vacuum=vacuum).center_around_origin( [0, 0, 0.5] ) info["interface"] = combined return info
class Atoms(object): """Generate Atoms python object.""" def __init__( self, lattice_mat=None, coords=None, elements=None, props=None, cartesian=False, show_props=False, ): """ Create atomic structure. Requires lattice, coordinates, atom type information. >>> box = [[2.715, 2.715, 0], [0, 2.715, 2.715], [2.715, 0, 2.715]] >>> coords = [[0, 0, 0], [0.25, 0.2, 0.25]] >>> elements = ["Si", "Si"] >>> Si = Atoms(lattice_mat=box, coords=coords, elements=elements) >>> print(round(Si.volume,2)) 40.03 >>> Si.composition {'Si': 2} >>> round(Si.density,2) 2.33 >>> round(Si.packing_fraction,2) 0.28 >>> Si.atomic_numbers [14, 14] >>> Si.num_atoms 2 >>> Si.frac_coords[0][0] 0 >>> Si.cart_coords[0][0] 0.0 >>> coords = [[0, 0, 0], [1.3575 , 1.22175, 1.22175]] >>> round(Si.density,2) 2.33 >>> Si.spacegroup() 'C2/m (12)' >>> Si.pymatgen_converter()!={} True """ self.lattice_mat = np.array(lattice_mat) self.show_props = show_props self.lattice = Lattice(lattice_mat) self.coords = np.array(coords) self.elements = elements self.cartesian = cartesian self.props = props if self.props is None: self.props = ["" for i in range(len(self.elements))] if self.cartesian: self.cart_coords = self.coords self.frac_coords = np.array(self.lattice.frac_coords(self.coords)) else: self.frac_coords = self.coords self.cart_coords = np.array(self.lattice.cart_coords(self.coords)) @property def check_polar(self): """ Check if the surface structure is polar. Comparing atom types at top and bottom. Applicable for sufcae with vaccums only. Args: file:atoms object (surface with vacuum) Returns: polar:True/False """ up = 0 dn = 0 coords = np.array(self.frac_coords) z_max = max(coords[:, 2]) z_min = min(coords[:, 2]) for site, element in zip(self.frac_coords, self.elements): if site[2] == z_max: up = up + Specie(element).Z if site[2] == z_min: dn = dn + Specie(element).Z polar = False if up != dn: print("Seems like a polar materials.") polar = True if up == dn: print("Non-polar") polar = False return polar def apply_strain(self, strain): """Apply a strain(e.g. 0.01, [0,0,.01]) to the lattice.""" s = (1 + np.array(strain)) * np.eye(3) self.lattice_mat = np.dot(self.lattice_mat.T, s).T def to_dict(self): """Provide dictionary representation of the atoms object.""" d = OrderedDict() d["lattice_mat"] = self.lattice_mat.tolist() d["coords"] = np.array(self.coords).tolist() d["elements"] = np.array(self.elements).tolist() d["abc"] = self.lattice.lat_lengths() d["angles"] = self.lattice.lat_angles() d["cartesian"] = self.cartesian d["props"] = self.props return d @classmethod def from_dict(self, d={}): """Form atoms object from the dictionary.""" return Atoms( lattice_mat=d["lattice_mat"], elements=d["elements"], props=d["props"], coords=d["coords"], cartesian=d["cartesian"], ) def remove_site_by_index(self, site=0): """Remove an atom by its index number.""" new_els = [] new_coords = [] new_props = [] for ii, i in enumerate(self.frac_coords): if ii != site: new_els.append(self.elements[ii]) new_coords.append(self.frac_coords[ii]) new_props.append(self.props[ii]) return Atoms( lattice_mat=self.lattice_mat, elements=new_els, coords=np.array(new_coords), props=new_props, cartesian=False, ) @property def get_primitive_atoms(self): """Get primitive Atoms using spacegroup information.""" from jarvis.analysis.structure.spacegroup import Spacegroup3D return Spacegroup3D(self).primitive_atoms @property def raw_distance_matrix(self): """Provide distance matrix.""" coords = np.array(self.cart_coords) z = (coords[:, None, :] - coords[None, :, :])**2 return np.sum(z, axis=-1)**0.5 def center(self, axis=2, vacuum=18.0, about=None): """ Center structure with vacuum padding. Args: vacuum:vacuum size axis: direction """ cell = self.lattice_mat p = self.cart_coords dirs = np.zeros_like(cell) for i in range(3): dirs[i] = np.cross(cell[i - 1], cell[i - 2]) dirs[i] /= np.sqrt(np.dot(dirs[i], dirs[i])) # normalize if np.dot(dirs[i], cell[i]) < 0.0: dirs[i] *= -1 if isinstance(axis, int): axes = (axis, ) else: axes = axis # if vacuum and any(self.pbc[x] for x in axes): # warnings.warn( # 'You are adding vacuum along a periodic direction!') # Now, decide how much each basis vector should be made longer longer = np.zeros(3) shift = np.zeros(3) for i in axes: p0 = np.dot(p, dirs[i]).min() if len(p) else 0 p1 = np.dot(p, dirs[i]).max() if len(p) else 0 height = np.dot(cell[i], dirs[i]) if vacuum is not None: lng = (p1 - p0 + 2 * vacuum) - height else: lng = 0.0 # Do not change unit cell size! top = lng + height - p1 shf = 0.5 * (top - p0) cosphi = np.dot(cell[i], dirs[i]) / np.sqrt( np.dot(cell[i], cell[i])) longer[i] = lng / cosphi shift[i] = shf / cosphi # Now, do it! translation = np.zeros(3) for i in axes: nowlen = np.sqrt(np.dot(cell[i], cell[i])) if vacuum is not None or cell[i].any(): cell[i] = cell[i] * (1 + longer[i] / nowlen) translation += shift[i] * cell[i] / nowlen new_coords = p + translation if about is not None: for vector in cell: new_coords -= vector / 2.0 new_coords += about atoms = Atoms( lattice_mat=cell, elements=self.elements, coords=new_coords, cartesian=True, ) return atoms @property def volume(self): """Get volume of the atoms object.""" m = self.lattice_mat vol = float(abs(np.dot(np.cross(m[0], m[1]), m[2]))) return vol @property def composition(self): """Get composition of the atoms object.""" comp = {} for i in self.elements: comp[i] = comp.setdefault(i, 0) + 1 return Composition(OrderedDict(comp)) @property def density(self): """Get density in g/cm3 of the atoms object.""" den = float(self.composition.weight * amu_gm) / (float(self.volume) * (ang_cm)**3) return den @property def atomic_numbers(self): """Get list of atomic numbers of atoms in the atoms object.""" numbers = [] for i in self.elements: numbers.append(Specie(i).Z) return numbers @property def num_atoms(self): """Get number of atoms.""" return len(self.coords) @property def uniq_species(self): """Get unique elements.""" return list(set(self.elements)) def get_center_of_mass(self): """Get center of mass of the atoms object.""" # atomic_mass m = [] for i in self.elements: m.append(Specie(i).atomic_mass) m = np.array(m) com = np.dot(m, self.cart_coords) / m.sum() # com = np.linalg.solve(self.lattice_mat.T, com) return com def get_origin(self): """Get center of mass of the atoms object.""" # atomic_mass return self.frac_coords.mean(axis=0) def center_around_origin(self, new_origin=[0.0, 0.0, 0.5]): """Center around given origin.""" lat = self.lattice_mat typ_sp = self.elements natoms = self.num_atoms # abc = self.lattice.lat_lengths() COM = self.get_origin() # COM = self.get_center_of_mass() x = np.zeros((natoms)) y = np.zeros((natoms)) z = np.zeros((natoms)) coords = list() for i in range(0, natoms): # cart_coords[i]=self.cart_coords[i]-COM x[i] = self.frac_coords[i][0] - COM[0] + new_origin[0] y[i] = self.frac_coords[i][1] - COM[1] + new_origin[1] z[i] = self.frac_coords[i][2] - COM[2] + new_origin[2] coords.append([x[i], y[i], z[i]]) struct = Atoms(lattice_mat=lat, elements=typ_sp, coords=coords, cartesian=False) return struct def pymatgen_converter(self): """Get pymatgen representation of the atoms object.""" try: from pymatgen.core.structure import Structure return Structure( self.lattice_mat, self.elements, self.frac_coords, coords_are_cartesian=False, ) except Exception: pass def spacegroup(self, symprec=1e-3): """Get spacegroup of the atoms object.""" import spglib sg = spglib.get_spacegroup( (self.lattice_mat, self.frac_coords, self.atomic_numbers), symprec=symprec, ) return sg @property def packing_fraction(self): """Get packing fraction of the atoms object.""" total_rad = 0 for i in self.elements: total_rad = total_rad + Specie(i).atomic_rad**3 pf = np.array([4 * np.pi * total_rad / (3 * self.volume)]) return round(pf[0], 5) def lattice_points_in_supercell(self, supercell_matrix): """ Adapted from Pymatgen. Returns the list of points on the original lattice contained in the supercell in fractional coordinates (with the supercell basis). e.g. [[2,0,0],[0,1,0],[0,0,1]] returns [[0,0,0],[0.5,0,0]] Args: supercell_matrix: 3x3 matrix describing the supercell Returns: numpy array of the fractional coordinates """ diagonals = np.array([ [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], ]) d_points = np.dot(diagonals, supercell_matrix) mins = np.min(d_points, axis=0) maxes = np.max(d_points, axis=0) + 1 ar = (np.arange(mins[0], maxes[0])[:, None] * np.array([1, 0, 0])[None, :]) br = (np.arange(mins[1], maxes[1])[:, None] * np.array([0, 1, 0])[None, :]) cr = (np.arange(mins[2], maxes[2])[:, None] * np.array([0, 0, 1])[None, :]) all_points = ar[:, None, None] + br[None, :, None] + cr[None, None, :] all_points = all_points.reshape((-1, 3)) frac_points = np.dot(all_points, np.linalg.inv(supercell_matrix)) tvects = frac_points[np.all(frac_points < 1 - 1e-10, axis=1) & np.all(frac_points >= -1e-10, axis=1)] assert len(tvects) == round(abs(np.linalg.det(supercell_matrix))) return tvects def make_supercell_matrix(self, scaling_matrix): """ Adapted from Pymatgen. Makes a supercell. Allowing to have sites outside the unit cell. Args: scaling_matrix: A scaling matrix for transforming the lattice vectors. Has to be all integers. Several options are possible: a. A full 3x3 scaling matrix defining the linear combination the old lattice vectors. E.g., [[2,1,0],[0,3,0],[0,0, 1]] generates a new structure with lattice vectors a' = 2a + b, b' = 3b, c' = c where a, b, and c are the lattice vectors of the original structure. b. An sequence of three scaling factors. E.g., [2, 1, 1] specifies that the supercell should have dimensions 2a x b x c. c. A number, which simply scales all lattice vectors by the same factor. Returns: Supercell structure. Note that a Structure is always returned, even if the input structure is a subclass of Structure. This is to avoid different arguments signatures from causing problems. If you prefer a subclass to return its own type, you need to override this method in the subclass. """ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) new_lattice = Lattice(np.dot(scale_matrix, self.lattice_mat)) f_lat = self.lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.cart_coords(f_lat) new_sites = [] new_elements = [] for site, el in zip(self.cart_coords, self.elements): for v in c_lat: new_elements.append(el) tmp = site + v new_sites.append(tmp) return Atoms( lattice_mat=new_lattice.lattice(), elements=new_elements, coords=new_sites, cartesian=True, ) def make_supercell(self, dim=[2, 2, 2]): """Make supercell of dimension dim.""" dim = np.array(dim) if dim.shape == (3, 3): dim = np.array([int(np.linalg.norm(v)) for v in dim]) coords = self.frac_coords all_symbs = self.elements # [i.symbol for i in s.species] nat = len(coords) new_nat = nat * dim[0] * dim[1] * dim[2] new_coords = np.zeros((new_nat, 3)) new_symbs = [] # np.chararray((new_nat)) props = [] # self.props ct = 0 for i in range(nat): for j in range(dim[0]): for k in range(dim[1]): for m in range(dim[2]): props.append(self.props[i]) new_coords[ct][0] = (coords[i][0] + j) / float(dim[0]) new_coords[ct][1] = (coords[i][1] + k) / float(dim[1]) new_coords[ct][2] = (coords[i][2] + m) / float(dim[2]) new_symbs.append(all_symbs[i]) ct = ct + 1 nat = new_nat nat = len(coords) # int(s.composition.num_atoms) lat = np.zeros((3, 3)) box = self.lattice_mat lat[0][0] = dim[0] * box[0][0] lat[0][1] = dim[0] * box[0][1] lat[0][2] = dim[0] * box[0][2] lat[1][0] = dim[1] * box[1][0] lat[1][1] = dim[1] * box[1][1] lat[1][2] = dim[1] * box[1][2] lat[2][0] = dim[2] * box[2][0] lat[2][1] = dim[2] * box[2][1] lat[2][2] = dim[2] * box[2][2] super_cell = Atoms( lattice_mat=lat, coords=new_coords, elements=new_symbs, props=props, cartesian=False, ) return super_cell def get_lll_reduced_structure(self): """Get LLL algorithm based reduced structure.""" reduced_latt = self.lattice.get_lll_reduced_lattice() if reduced_latt != self.lattice: return Atoms( lattice_mat=reduced_latt._lat, elements=self.elements, coords=self.frac_coords, cartesian=False, ) else: return Atoms( lattice_mat=self.lattice_mat, elements=self.elements, coords=self.frac_coords, cartesian=False, ) def __repr__(self): """Get representation during print statement.""" return self.get_string() def get_string(self, cart=True, sort_order="X"): """ Convert Atoms to string. Optional arguments below. Args: cart:True/False for cartesian/fractional coords. sort_order: sort by chemical properties of elements. Default electronegativity. """ system = str(self.composition.formula) header = (str(system) + str("\n1.0\n") + str(self.lattice_mat[0][0]) + " " + str(self.lattice_mat[0][1]) + " " + str(self.lattice_mat[0][2]) + "\n" + str(self.lattice_mat[1][0]) + " " + str(self.lattice_mat[1][1]) + " " + str(self.lattice_mat[1][2]) + "\n" + str(self.lattice_mat[2][0]) + " " + str(self.lattice_mat[2][1]) + " " + str(self.lattice_mat[2][2]) + "\n") if sort_order is None: order = np.argsort(self.elements) else: order = np.argsort([ Specie(i).element_property(sort_order) for i in self.elements ]) if cart: coords = self.cart_coords else: coords = self.frac_coords coords_ordered = np.array(coords)[order] elements_ordered = np.array(self.elements)[order] props_ordered = np.array(self.props)[order] # check_selective_dynamics = False # TODO counts = get_counts(elements_ordered) cart_frac = "" if cart: cart_frac = "\nCartesian\n" else: cart_frac = "\nDirect\n" if "T" in "".join(map(str, self.props[0])): middle = (" ".join(map(str, counts.keys())) + "\n" + " ".join(map(str, counts.values())) + "\nSelective dynamics\n" + cart_frac) else: middle = (" ".join(map(str, counts.keys())) + "\n" + " ".join(map(str, counts.values())) + cart_frac) rest = "" for ii, i in enumerate(coords_ordered): if self.show_props: rest = (rest + " ".join(map(str, i)) + " " + str(props_ordered[ii]) + "\n") else: rest = rest + " ".join(map(str, i)) + "\n" result = header + middle + rest return result
def test_lat(): box = [[10, 0, 0], [0, 10, 0], [0, 0, 10]] lat = Lattice(box) td = lat.to_dict() fd = Lattice.from_dict(td) frac_coords = [[0, 0, 0], [0.5, 0.5, 0.5]] cart_coords = [[0, 0, 0], [5, 5, 5]] lll = lat._calculate_lll() # print ('lll',lll[0][0][0]) lll_red = lat.get_lll_reduced_lattice()._lat # print("lll_educed", lat.get_lll_reduced_lattice()._lat[0][0]) assert ( lat.lat_lengths(), lat.lat_angles(), round(lat.inv_lattice()[0][0], 2), [round(i, 2) for i in lat.lat_angles(radians=True)], lat.cart_coords(frac_coords)[1][1], lat.frac_coords(cart_coords)[1][1], lat.volume, lat.parameters, lll[0][0][0], lll_red[0][0], ) == ( [10.0, 10.0, 10.0], [90.0, 90.0, 90.0], 0.1, [1.57, 1.57, 1.57], 5.0, 0.5, 1000.0, [10.0, 10.0, 10.0, 90.0, 90.0, 90.0], 10.0, 10.0, ) d = data('dft_3d') for i in d: if i['jid'] == 'JVASP-588': atoms = Atoms.from_dict(i['atoms']) lll = atoms.lattice._calculate_lll() assert lll[1][0][0] == -1
class Atoms(object): """Generate Atoms python object.""" def __init__( self, lattice_mat=None, coords=None, elements=None, props=None, cartesian=False, show_props=False, ): """ Create atomic structure. Requires lattice, coordinates, atom type information. >>> box = [[2.715, 2.715, 0], [0, 2.715, 2.715], [2.715, 0, 2.715]] >>> coords = [[0, 0, 0], [0.25, 0.2, 0.25]] >>> elements = ["Si", "Si"] >>> Si = Atoms(lattice_mat=box, coords=coords, elements=elements) >>> print(round(Si.volume,2)) 40.03 >>> Si.composition {'Si': 2} >>> round(Si.density,2) 2.33 >>> round(Si.packing_fraction,2) 0.28 >>> Si.atomic_numbers [14, 14] >>> Si.num_atoms 2 >>> Si.frac_coords[0][0] 0 >>> Si.cart_coords[0][0] 0.0 >>> coords = [[0, 0, 0], [1.3575 , 1.22175, 1.22175]] >>> round(Si.density,2) 2.33 >>> Si.spacegroup() 'C2/m (12)' >>> Si.pymatgen_converter()!={} True """ self.lattice_mat = np.array(lattice_mat) self.show_props = show_props self.lattice = Lattice(lattice_mat) self.coords = np.array(coords) self.elements = elements self.cartesian = cartesian self.props = props if self.props is None: self.props = ["" for i in range(len(self.elements))] if self.cartesian: self.cart_coords = self.coords self.frac_coords = np.array(self.lattice.frac_coords(self.coords)) else: self.frac_coords = self.coords self.cart_coords = np.array(self.lattice.cart_coords(self.coords)) def write_cif( self, filename="atoms.cif", comment=None, with_spg_info=True ): """ Write CIF format file from Atoms object. Caution: can't handle fractional occupancies right now """ if comment is None: comment = "CIF file written using JARVIS-Tools package." comment = comment + "\n" f = open(filename, "w") f.write(comment) composition = self.composition line = "data_" + str(composition.reduced_formula) + "\n" f.write(line) from jarvis.analysis.structure.spacegroup import Spacegroup3D if with_spg_info: spg = Spacegroup3D(self) line = ( "_symmetry_space_group_name_H-M " + str("'") + str(spg.space_group_symbol) + str("'") + "\n" ) else: line = "_symmetry_space_group_name_H-M " + str("'P 1'") + "\n" f.write(line) a, b, c, alpha, beta, gamma = self.lattice.parameters f.write("_cell_length_a %g\n" % a) f.write("_cell_length_b %g\n" % b) f.write("_cell_length_c %g\n" % c) f.write("_cell_angle_alpha %g\n" % alpha) f.write("_cell_angle_beta %g\n" % beta) f.write("_cell_angle_gamma %g\n" % gamma) f.write("\n") if with_spg_info: line = ( "_symmetry_Int_Tables_number " + str(spg.space_group_number) + "\n" ) else: line = "_symmetry_Int_Tables_number " + str(1) + "\n" f.write(line) line = ( "_chemical_formula_structural " + str(composition.reduced_formula) + "\n" ) f.write(line) line = "_chemical_formula_sum " + str(composition.formula) + "\n" f.write(line) line = "_cell_volume " + str(self.volume) + "\n" f.write(line) reduced, repeat = composition.reduce() line = "_cell_formula_units_Z " + str(repeat) + "\n" f.write(line) f.write("loop_\n") f.write(" _symmetry_equiv_pos_site_id\n") f.write(" _symmetry_equiv_pos_as_xyz\n") f.write(" 1 'x, y, z'\n") f.write("loop_\n") f.write(" _atom_site_type_symbol\n") f.write(" _atom_site_label\n") f.write(" _atom_site_symmetry_multiplicity\n") f.write(" _atom_site_fract_x\n") f.write(" _atom_site_fract_y\n") f.write(" _atom_site_fract_z\n") f.write(" _atom_site_fract_occupancy\n") order = np.argsort(self.elements) coords_ordered = np.array(self.frac_coords)[order] elements_ordered = np.array(self.elements)[order] occ = 1 extra = 1 element_types = [] # count = 0 for ii, i in enumerate(elements_ordered): if i not in element_types: element_types.append(i) count = 0 symbol = i count = count + 1 label = str(i) + str(count) element_types.append(i) coords = coords_ordered[ii] f.write( " %s %s %s %7.5f %7.5f %7.5f %s\n" % (symbol, label, occ, coords[0], coords[1], coords[2], extra) ) f.close() @staticmethod def from_cif(filename="atoms.cif"): """Read .cif format file.""" # Warnings: # May not work for: # system with partial occupancy # cif file with multiple blocks # _atom_site_U_iso, instead of fractn_x, cartn_x # with non-zero _atom_site_attached_hydrogens f = open(filename, "r") lines = f.read().splitlines() f.close() lat_a = "" lat_b = "" lat_c = "" lat_alpha = "" lat_beta = "" lat_gamma = "" # TODO: check if chemical_formula_sum # matches Atoms.compsotion.reduced_formula # chemical_formula_structural = "" # chemical_formula_sum = "" # chemical_name_mineral = "" sym_xyz_line = "" for ii, i in enumerate(lines): if "_cell_length_a" in i: lat_a = float(i.split()[1].split("(")[0]) if "_cell_length_b" in i: lat_b = float(i.split()[1].split("(")[0]) if "_cell_length_c" in i: lat_c = float(i.split()[1].split("(")[0]) if "_cell_angle_alpha" in i: lat_alpha = float(i.split()[1].split("(")[0]) if "_cell_angle_beta" in i: lat_beta = float(i.split()[1].split("(")[0]) if "_cell_angle_gamma" in i: lat_gamma = float(i.split()[1].split("(")[0]) # if "_chemical_formula_structural" in i: # chemical_formula_structural = i.split()[1] # if "_chemical_formula_sum" in i: # chemical_formula_sum = i.split()[1] # if "_chemical_name_mineral" in i: # chemical_name_mineral = i.split()[1] if "_symmetry_equiv_pos_as_xyz" in i: sym_xyz_line = ii if "_symmetry_equiv_pos_as_xyz_" in i: sym_xyz_line = ii if "_symmetry_equiv_pos_as_xyz_" in i: sym_xyz_line = ii if "_space_group_symop_operation_xyz_" in i: sym_xyz_line = ii if "_space_group_symop_operation_xyz" in i: sym_xyz_line = ii symm_ops = [] terminate = False count = 0 while not terminate: print("sym_xyz_line", sym_xyz_line) tmp = lines[sym_xyz_line + count + 1] if "x" in tmp and "y" in tmp and "z" in tmp: # print("tmp", tmp) symm_ops.append(tmp) count += 1 else: terminate = True tmp_arr = [lat_a, lat_b, lat_c, lat_alpha, lat_beta, lat_gamma] if any(ele == "" for ele in tmp_arr): raise ValueError("Lattice information is incomplete.", tmp_arr) lat = Lattice.from_parameters( lat_a, lat_b, lat_c, lat_alpha, lat_beta, lat_gamma ) terminate = False atom_features = [] count = 0 beginning_atom_info_line = 0 for ii, i in enumerate(lines): if "loop_" in i and "_atom_site" in lines[ii + count + 1]: beginning_atom_info_line = ii while not terminate: if "_atom" in lines[beginning_atom_info_line + count + 1]: atom_features.append( lines[beginning_atom_info_line + count + 1] ) count += 1 if "_atom" not in lines[beginning_atom_info_line + count]: terminate = True terminate = False count = 1 atom_liines = [] while not terminate: number = beginning_atom_info_line + len(atom_features) + count if number == len(lines): terminate = True break line = lines[number] # print ('tis line',line) if len(line.split()) == len(atom_features): atom_liines.append(line) count += 1 else: terminate = True label_index = "" fract_x_index = "" fract_y_index = "" fract_z_index = "" cartn_x_index = "" cartn_y_index = "" cartn_z_index = "" occupancy_index = "" for ii, i in enumerate(atom_features): if "_atom_site_label" in i: label_index = ii if "fract_x" in i: fract_x_index = ii if "fract_y" in i: fract_y_index = ii if "fract_z" in i: fract_z_index = ii if "cartn_x" in i: cartn_x_index = ii if "cartn_y" in i: cartn_y_index = ii if "cartn_z" in i: cartn_z_index = ii if "occupancy" in i: occupancy_index = ii if fract_x_index == "" and cartn_x_index == "": raise ValueError("Cannot find atomic coordinate info.") elements = [] coords = [] cif_atoms = None if fract_x_index != "": for ii, i in enumerate(atom_liines): tmp = i.split() tmp_lbl = list( Composition.from_string(tmp[label_index]).to_dict().keys() ) elem = tmp_lbl[0] coord = [ float(tmp[fract_x_index].split("(")[0]), float(tmp[fract_y_index].split("(")[0]), float(tmp[fract_z_index].split("(")[0]), ] if len(tmp_lbl) > 1: raise ValueError("Check if labesl are correct.", tmp_lbl) if ( occupancy_index != "" and not float( tmp[occupancy_index].split("(")[0] ).is_integer() ): raise ValueError( "Fractional occupancy is not supported.", float(tmp[occupancy_index].split("(")[0]), elem, ) elements.append(elem) coords.append(coord) cif_atoms = Atoms( lattice_mat=lat.matrix, elements=elements, coords=coords, cartesian=False, ) elif cartn_x_index != "": for ii, i in enumerate(atom_liines): tmp = i.split() tmp_lbl = list( Composition.from_string(tmp[label_index]).to_dict().keys() ) elem = tmp_lbl[0] coord = [ float(tmp[cartn_x_index].split("(")[0]), float(tmp[cartn_y_index].split("(")[0]), float(tmp[cartn_z_index].split("(")[0]), ] if len(tmp_lbl) > 1: raise ValueError("Check if labesl are correct.", tmp_lbl) if ( occupancy_index != "" and not float( tmp[occupancy_index].split("(")[0] ).is_integer() ): raise ValueError( "Fractional occupancy is not supported.", float(tmp[occupancy_index].split("(")[0]), elem, ) elements.append(elem) coords.append(coord) cif_atoms = Atoms( lattice_mat=lat.matrix, elements=elements, coords=coords, cartesian=True, ) else: raise ValueError( "Cannot find atomic coordinate info from cart or frac." ) # frac_coords=list(cif_atoms.frac_coords) cif_elements = cif_atoms.elements lat = cif_atoms.lattice.matrix if len(symm_ops) > 1: frac_coords = list(cif_atoms.frac_coords) for i in symm_ops: for jj, j in enumerate(frac_coords): new_c_coord = get_new_coord_for_xyz_sym( xyz_string=i, frac_coord=j ) new_frac_coord = [new_c_coord][0] if not check_duplicate_coords(frac_coords, new_frac_coord): frac_coords.append(new_frac_coord) cif_elements.append(cif_elements[jj]) new_atoms = Atoms( lattice_mat=lat, coords=frac_coords, elements=cif_elements, cartesian=False, ) cif_atoms = new_atoms return cif_atoms def write_poscar(self, filename="POSCAR"): """Write POSCAR format file from Atoms object.""" from jarvis.io.vasp.inputs import Poscar pos = Poscar(self) pos.write_file(filename) @property def get_xyz_string(self): """Get xyz string for atoms.""" line = str(self.num_atoms) + "\n" line += " ".join(map(str, np.array(self.lattice_mat).flatten())) + "\n" for i, j in zip(self.elements, self.cart_coords): line += ( str(i) + str(" ") + str(round(j[0], 4)) + str(" ") + str(round(j[1], 4)) + str(" ") + str(round(j[2], 4)) + "\n" ) return line def write_xyz(self, filename="atoms.xyz"): """Write XYZ format file.""" f = open(filename, "w") line = str(self.num_atoms) + "\n" f.write(line) line = ",".join(map(str, np.array(self.lattice_mat).flatten())) + "\n" f.write(line) for i, j in zip(self.elements, self.cart_coords): f.write("%s %7.5f %7.5f %7.5f\n" % (i, j[0], j[1], j[2])) f.close() @classmethod def from_xyz(self, filename="dsgdb9nsd_057387.xyz", box_size=40): """Read XYZ file from to make Atoms object.""" lattice_mat = [[box_size, 0, 0], [0, box_size, 0], [0, 0, box_size]] f = open(filename, "r") lines = f.read().splitlines() f.close() coords = [] species = [] natoms = int(lines[0]) for i in range(natoms): tmp = (lines[i + 2]).split() coord = [(tmp[1]), (tmp[2]), (tmp[3])] coord = [ 0 if "*" in ii else float(ii) for ii in coord ] # dsgdb9nsd_000212.xyz coords.append(coord) species.append(tmp[0]) coords = np.array(coords) atoms = Atoms( lattice_mat=lattice_mat, coords=coords, elements=species, cartesian=True, ).center_around_origin(new_origin=[0.5, 0.5, 0.5]) # print (atoms) return atoms @classmethod def from_poscar(self, filename="POSCAR"): """Read POSCAR/CONTCAR file from to make Atoms object.""" from jarvis.io.vasp.inputs import Poscar return Poscar.from_file(filename).atoms @property def check_polar(self): """ Check if the surface structure is polar. Comparing atom types at top and bottom. Applicable for sufcae with vaccums only. Args: file:atoms object (surface with vacuum) Returns: polar:True/False """ up = 0 dn = 0 coords = np.array(self.frac_coords) z_max = max(coords[:, 2]) z_min = min(coords[:, 2]) for site, element in zip(self.frac_coords, self.elements): if site[2] == z_max: up = up + Specie(element).Z if site[2] == z_min: dn = dn + Specie(element).Z polar = False if up != dn: print("Seems like a polar materials.") polar = True if up == dn: print("Non-polar") polar = False return polar def apply_strain(self, strain): """Apply a strain(e.g. 0.01, [0,0,.01]) to the lattice.""" s = (1 + np.array(strain)) * np.eye(3) self.lattice_mat = np.dot(self.lattice_mat.T, s).T def to_dict(self): """Provide dictionary representation of the atoms object.""" d = OrderedDict() d["lattice_mat"] = self.lattice_mat.tolist() d["coords"] = np.array(self.coords).tolist() d["elements"] = np.array(self.elements).tolist() d["abc"] = self.lattice.lat_lengths() d["angles"] = self.lattice.lat_angles() d["cartesian"] = self.cartesian d["props"] = self.props return d @classmethod def from_dict(self, d={}): """Form atoms object from the dictionary.""" return Atoms( lattice_mat=d["lattice_mat"], elements=d["elements"], props=d["props"], coords=d["coords"], cartesian=d["cartesian"], ) def remove_site_by_index(self, site=0): """Remove an atom by its index number.""" new_els = [] new_coords = [] new_props = [] for ii, i in enumerate(self.frac_coords): if ii != site: new_els.append(self.elements[ii]) new_coords.append(self.frac_coords[ii]) new_props.append(self.props[ii]) return Atoms( lattice_mat=self.lattice_mat, elements=new_els, coords=np.array(new_coords), props=new_props, cartesian=False, ) @property def get_primitive_atoms(self): """Get primitive Atoms using spacegroup information.""" from jarvis.analysis.structure.spacegroup import Spacegroup3D return Spacegroup3D(self).primitive_atoms @property def raw_distance_matrix(self): """Provide distance matrix.""" coords = np.array(self.cart_coords) z = (coords[:, None, :] - coords[None, :, :]) ** 2 return np.sum(z, axis=-1) ** 0.5 @property def raw_angle_matrix(self, cut_off=5.0): """Provide distance matrix.""" coords = np.array(self.cart_coords) angles = [] for a, b, c in itertools.product(coords, coords, coords): if ( not np.array_equal(a, b) and not np.array_equal(b, c) and np.linalg.norm((a - b)) < cut_off and np.linalg.norm((c - b)) < cut_off ): angle = get_angle(a, b, c) angles.append(angle) return angles def center(self, axis=2, vacuum=18.0, about=None): """ Center structure with vacuum padding. Args: vacuum:vacuum size axis: direction """ cell = self.lattice_mat p = self.cart_coords dirs = np.zeros_like(cell) for i in range(3): dirs[i] = np.cross(cell[i - 1], cell[i - 2]) dirs[i] /= np.sqrt(np.dot(dirs[i], dirs[i])) # normalize if np.dot(dirs[i], cell[i]) < 0.0: dirs[i] *= -1 if isinstance(axis, int): axes = (axis,) else: axes = axis # if vacuum and any(self.pbc[x] for x in axes): # warnings.warn( # 'You are adding vacuum along a periodic direction!') # Now, decide how much each basis vector should be made longer longer = np.zeros(3) shift = np.zeros(3) for i in axes: p0 = np.dot(p, dirs[i]).min() if len(p) else 0 p1 = np.dot(p, dirs[i]).max() if len(p) else 0 height = np.dot(cell[i], dirs[i]) if vacuum is not None: lng = (p1 - p0 + 2 * vacuum) - height else: lng = 0.0 # Do not change unit cell size! top = lng + height - p1 shf = 0.5 * (top - p0) cosphi = np.dot(cell[i], dirs[i]) / np.sqrt( np.dot(cell[i], cell[i]) ) longer[i] = lng / cosphi shift[i] = shf / cosphi # Now, do it! translation = np.zeros(3) for i in axes: nowlen = np.sqrt(np.dot(cell[i], cell[i])) if vacuum is not None or cell[i].any(): cell[i] = cell[i] * (1 + longer[i] / nowlen) translation += shift[i] * cell[i] / nowlen new_coords = p + translation if about is not None: for vector in cell: new_coords -= vector / 2.0 new_coords += about atoms = Atoms( lattice_mat=cell, elements=self.elements, coords=new_coords, cartesian=True, ) return atoms @property def volume(self): """Get volume of the atoms object.""" m = self.lattice_mat vol = float(abs(np.dot(np.cross(m[0], m[1]), m[2]))) return vol @property def composition(self): """Get composition of the atoms object.""" comp = {} for i in self.elements: comp[i] = comp.setdefault(i, 0) + 1 return Composition(OrderedDict(comp)) @property def density(self): """Get density in g/cm3 of the atoms object.""" den = float(self.composition.weight * amu_gm) / ( float(self.volume) * (ang_cm) ** 3 ) return den @property def atomic_numbers(self): """Get list of atomic numbers of atoms in the atoms object.""" numbers = [] for i in self.elements: numbers.append(Specie(i).Z) return numbers @property def num_atoms(self): """Get number of atoms.""" return len(self.coords) @property def uniq_species(self): """Get unique elements.""" uniq = set() return [x for x in self.elements if not (x in uniq or uniq.add(x))] def get_center_of_mass(self): """Get center of mass of the atoms object.""" # atomic_mass m = [] for i in self.elements: m.append(Specie(i).atomic_mass) m = np.array(m) com = np.dot(m, self.cart_coords) / m.sum() # com = np.linalg.solve(self.lattice_mat.T, com) return com def get_origin(self): """Get center of mass of the atoms object.""" # atomic_mass return self.frac_coords.mean(axis=0) def center_around_origin(self, new_origin=[0.0, 0.0, 0.5]): """Center around given origin.""" lat = self.lattice_mat typ_sp = self.elements natoms = self.num_atoms # abc = self.lattice.lat_lengths() COM = self.get_origin() # COM = self.get_center_of_mass() x = np.zeros((natoms)) y = np.zeros((natoms)) z = np.zeros((natoms)) coords = list() for i in range(0, natoms): # cart_coords[i]=self.cart_coords[i]-COM x[i] = self.frac_coords[i][0] - COM[0] + new_origin[0] y[i] = self.frac_coords[i][1] - COM[1] + new_origin[1] z[i] = self.frac_coords[i][2] - COM[2] + new_origin[2] coords.append([x[i], y[i], z[i]]) struct = Atoms( lattice_mat=lat, elements=typ_sp, coords=coords, cartesian=False ) return struct def pymatgen_converter(self): """Get pymatgen representation of the atoms object.""" try: from pymatgen.core.structure import Structure return Structure( self.lattice_mat, self.elements, self.frac_coords, coords_are_cartesian=False, ) except Exception: print("Requires pymatgen for this functionality.") pass def phonopy_converter(self, pbc=True): """Get phonopy representation of the atoms object.""" try: from phonopy.structure.atoms import Atoms as PhonopyAtoms return PhonopyAtoms( symbols=self.elements, positions=self.cart_coords, pbc=pbc, cell=self.lattice_mat, ) except Exception: print("Requires phonopy for this functionality.") pass def ase_converter(self, pbc=True): """Get ASE representation of the atoms object.""" try: from ase import Atoms as AseAtoms return AseAtoms( symbols=self.elements, positions=self.cart_coords, pbc=pbc, cell=self.lattice_mat, ) except Exception: print("Requires ASE for this functionality.") pass def spacegroup(self, symprec=1e-3): """Get spacegroup of the atoms object.""" import spglib sg = spglib.get_spacegroup( (self.lattice_mat, self.frac_coords, self.atomic_numbers), symprec=symprec, ) return sg @property def packing_fraction(self): """Get packing fraction of the atoms object.""" total_rad = 0 for i in self.elements: total_rad = total_rad + Specie(i).atomic_rad ** 3 pf = np.array([4 * np.pi * total_rad / (3 * self.volume)]) return round(pf[0], 5) def lattice_points_in_supercell(self, supercell_matrix): """ Adapted from Pymatgen. Returns the list of points on the original lattice contained in the supercell in fractional coordinates (with the supercell basis). e.g. [[2,0,0],[0,1,0],[0,0,1]] returns [[0,0,0],[0.5,0,0]] Args: supercell_matrix: 3x3 matrix describing the supercell Returns: numpy array of the fractional coordinates """ diagonals = np.array( [ [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], ] ) d_points = np.dot(diagonals, supercell_matrix) mins = np.min(d_points, axis=0) maxes = np.max(d_points, axis=0) + 1 ar = ( np.arange(mins[0], maxes[0])[:, None] * np.array([1, 0, 0])[None, :] ) br = ( np.arange(mins[1], maxes[1])[:, None] * np.array([0, 1, 0])[None, :] ) cr = ( np.arange(mins[2], maxes[2])[:, None] * np.array([0, 0, 1])[None, :] ) all_points = ar[:, None, None] + br[None, :, None] + cr[None, None, :] all_points = all_points.reshape((-1, 3)) frac_points = np.dot(all_points, np.linalg.inv(supercell_matrix)) tvects = frac_points[ np.all(frac_points < 1 - 1e-10, axis=1) & np.all(frac_points >= -1e-10, axis=1) ] assert len(tvects) == round(abs(np.linalg.det(supercell_matrix))) return tvects def make_supercell_matrix(self, scaling_matrix): """ Adapted from Pymatgen. Makes a supercell. Allowing to have sites outside the unit cell. Args: scaling_matrix: A scaling matrix for transforming the lattice vectors. Has to be all integers. Several options are possible: a. A full 3x3 scaling matrix defining the linear combination the old lattice vectors. E.g., [[2,1,0],[0,3,0],[0,0, 1]] generates a new structure with lattice vectors a' = 2a + b, b' = 3b, c' = c where a, b, and c are the lattice vectors of the original structure. b. An sequence of three scaling factors. E.g., [2, 1, 1] specifies that the supercell should have dimensions 2a x b x c. c. A number, which simply scales all lattice vectors by the same factor. Returns: Supercell structure. Note that a Structure is always returned, even if the input structure is a subclass of Structure. This is to avoid different arguments signatures from causing problems. If you prefer a subclass to return its own type, you need to override this method in the subclass. """ scale_matrix = np.array(scaling_matrix, np.int16) if scale_matrix.shape != (3, 3): scale_matrix = np.array(scale_matrix * np.eye(3), np.int16) new_lattice = Lattice(np.dot(scale_matrix, self.lattice_mat)) f_lat = self.lattice_points_in_supercell(scale_matrix) c_lat = new_lattice.cart_coords(f_lat) new_sites = [] new_elements = [] for site, el in zip(self.cart_coords, self.elements): for v in c_lat: new_elements.append(el) tmp = site + v new_sites.append(tmp) return Atoms( lattice_mat=new_lattice.lattice(), elements=new_elements, coords=new_sites, cartesian=True, ) def make_supercell(self, dim=[2, 2, 2]): """Make supercell of dimension dim.""" dim = np.array(dim) if dim.shape == (3, 3): dim = np.array([int(np.linalg.norm(v)) for v in dim]) coords = self.frac_coords all_symbs = self.elements # [i.symbol for i in s.species] nat = len(coords) new_nat = nat * dim[0] * dim[1] * dim[2] new_coords = np.zeros((new_nat, 3)) new_symbs = [] # np.chararray((new_nat)) props = [] # self.props ct = 0 for i in range(nat): for j in range(dim[0]): for k in range(dim[1]): for m in range(dim[2]): props.append(self.props[i]) new_coords[ct][0] = (coords[i][0] + j) / float(dim[0]) new_coords[ct][1] = (coords[i][1] + k) / float(dim[1]) new_coords[ct][2] = (coords[i][2] + m) / float(dim[2]) new_symbs.append(all_symbs[i]) ct = ct + 1 nat = new_nat nat = len(coords) # int(s.composition.num_atoms) lat = np.zeros((3, 3)) box = self.lattice_mat lat[0][0] = dim[0] * box[0][0] lat[0][1] = dim[0] * box[0][1] lat[0][2] = dim[0] * box[0][2] lat[1][0] = dim[1] * box[1][0] lat[1][1] = dim[1] * box[1][1] lat[1][2] = dim[1] * box[1][2] lat[2][0] = dim[2] * box[2][0] lat[2][1] = dim[2] * box[2][1] lat[2][2] = dim[2] * box[2][2] super_cell = Atoms( lattice_mat=lat, coords=new_coords, elements=new_symbs, props=props, cartesian=False, ) return super_cell def get_lll_reduced_structure(self): """Get LLL algorithm based reduced structure.""" reduced_latt = self.lattice.get_lll_reduced_lattice() if reduced_latt != self.lattice: return Atoms( lattice_mat=reduced_latt._lat, elements=self.elements, coords=self.frac_coords, cartesian=False, ) else: return Atoms( lattice_mat=self.lattice_mat, elements=self.elements, coords=self.frac_coords, cartesian=False, ) def __repr__(self): """Get representation during print statement.""" return self.get_string() def get_string(self, cart=True, sort_order="X"): """ Convert Atoms to string. Optional arguments below. Args: cart:True/False for cartesian/fractional coords. sort_order: sort by chemical properties of elements. Default electronegativity. """ system = str(self.composition.formula) header = ( str(system) + str("\n1.0\n") + str(self.lattice_mat[0][0]) + " " + str(self.lattice_mat[0][1]) + " " + str(self.lattice_mat[0][2]) + "\n" + str(self.lattice_mat[1][0]) + " " + str(self.lattice_mat[1][1]) + " " + str(self.lattice_mat[1][2]) + "\n" + str(self.lattice_mat[2][0]) + " " + str(self.lattice_mat[2][1]) + " " + str(self.lattice_mat[2][2]) + "\n" ) if sort_order is None: order = np.argsort(self.elements) else: order = np.argsort( [Specie(i).element_property(sort_order) for i in self.elements] ) if cart: coords = self.cart_coords else: coords = self.frac_coords coords_ordered = np.array(coords)[order] elements_ordered = np.array(self.elements)[order] props_ordered = np.array(self.props)[order] # check_selective_dynamics = False # TODO counts = get_counts(elements_ordered) cart_frac = "" if cart: cart_frac = "\nCartesian\n" else: cart_frac = "\nDirect\n" if "T" in "".join(map(str, self.props[0])): middle = ( " ".join(map(str, counts.keys())) + "\n" + " ".join(map(str, counts.values())) + "\nSelective dynamics\n" + cart_frac ) else: middle = ( " ".join(map(str, counts.keys())) + "\n" + " ".join(map(str, counts.values())) + cart_frac ) rest = "" if coords_ordered.ndim == 1: coords_ordered = np.array([coords]) for ii, i in enumerate(coords_ordered): if self.show_props: rest = ( rest + " ".join(map(str, i)) + " " + str(props_ordered[ii]) + "\n" ) else: rest = rest + " ".join(map(str, i)) + "\n" result = header + middle + rest return result
def from_cif(filename="atoms.cif"): """Read .cif format file.""" # Warnings: # May not work for: # system with partial occupancy # cif file with multiple blocks # _atom_site_U_iso, instead of fractn_x, cartn_x # with non-zero _atom_site_attached_hydrogens f = open(filename, "r") lines = f.read().splitlines() f.close() lat_a = "" lat_b = "" lat_c = "" lat_alpha = "" lat_beta = "" lat_gamma = "" # TODO: check if chemical_formula_sum # matches Atoms.compsotion.reduced_formula # chemical_formula_structural = "" # chemical_formula_sum = "" # chemical_name_mineral = "" sym_xyz_line = "" for ii, i in enumerate(lines): if "_cell_length_a" in i: lat_a = float(i.split()[1].split("(")[0]) if "_cell_length_b" in i: lat_b = float(i.split()[1].split("(")[0]) if "_cell_length_c" in i: lat_c = float(i.split()[1].split("(")[0]) if "_cell_angle_alpha" in i: lat_alpha = float(i.split()[1].split("(")[0]) if "_cell_angle_beta" in i: lat_beta = float(i.split()[1].split("(")[0]) if "_cell_angle_gamma" in i: lat_gamma = float(i.split()[1].split("(")[0]) # if "_chemical_formula_structural" in i: # chemical_formula_structural = i.split()[1] # if "_chemical_formula_sum" in i: # chemical_formula_sum = i.split()[1] # if "_chemical_name_mineral" in i: # chemical_name_mineral = i.split()[1] if "_symmetry_equiv_pos_as_xyz" in i: sym_xyz_line = ii if "_symmetry_equiv_pos_as_xyz_" in i: sym_xyz_line = ii if "_symmetry_equiv_pos_as_xyz_" in i: sym_xyz_line = ii if "_space_group_symop_operation_xyz_" in i: sym_xyz_line = ii if "_space_group_symop_operation_xyz" in i: sym_xyz_line = ii symm_ops = [] terminate = False count = 0 while not terminate: print("sym_xyz_line", sym_xyz_line) tmp = lines[sym_xyz_line + count + 1] if "x" in tmp and "y" in tmp and "z" in tmp: # print("tmp", tmp) symm_ops.append(tmp) count += 1 else: terminate = True tmp_arr = [lat_a, lat_b, lat_c, lat_alpha, lat_beta, lat_gamma] if any(ele == "" for ele in tmp_arr): raise ValueError("Lattice information is incomplete.", tmp_arr) lat = Lattice.from_parameters( lat_a, lat_b, lat_c, lat_alpha, lat_beta, lat_gamma ) terminate = False atom_features = [] count = 0 beginning_atom_info_line = 0 for ii, i in enumerate(lines): if "loop_" in i and "_atom_site" in lines[ii + count + 1]: beginning_atom_info_line = ii while not terminate: if "_atom" in lines[beginning_atom_info_line + count + 1]: atom_features.append( lines[beginning_atom_info_line + count + 1] ) count += 1 if "_atom" not in lines[beginning_atom_info_line + count]: terminate = True terminate = False count = 1 atom_liines = [] while not terminate: number = beginning_atom_info_line + len(atom_features) + count if number == len(lines): terminate = True break line = lines[number] # print ('tis line',line) if len(line.split()) == len(atom_features): atom_liines.append(line) count += 1 else: terminate = True label_index = "" fract_x_index = "" fract_y_index = "" fract_z_index = "" cartn_x_index = "" cartn_y_index = "" cartn_z_index = "" occupancy_index = "" for ii, i in enumerate(atom_features): if "_atom_site_label" in i: label_index = ii if "fract_x" in i: fract_x_index = ii if "fract_y" in i: fract_y_index = ii if "fract_z" in i: fract_z_index = ii if "cartn_x" in i: cartn_x_index = ii if "cartn_y" in i: cartn_y_index = ii if "cartn_z" in i: cartn_z_index = ii if "occupancy" in i: occupancy_index = ii if fract_x_index == "" and cartn_x_index == "": raise ValueError("Cannot find atomic coordinate info.") elements = [] coords = [] cif_atoms = None if fract_x_index != "": for ii, i in enumerate(atom_liines): tmp = i.split() tmp_lbl = list( Composition.from_string(tmp[label_index]).to_dict().keys() ) elem = tmp_lbl[0] coord = [ float(tmp[fract_x_index].split("(")[0]), float(tmp[fract_y_index].split("(")[0]), float(tmp[fract_z_index].split("(")[0]), ] if len(tmp_lbl) > 1: raise ValueError("Check if labesl are correct.", tmp_lbl) if ( occupancy_index != "" and not float( tmp[occupancy_index].split("(")[0] ).is_integer() ): raise ValueError( "Fractional occupancy is not supported.", float(tmp[occupancy_index].split("(")[0]), elem, ) elements.append(elem) coords.append(coord) cif_atoms = Atoms( lattice_mat=lat.matrix, elements=elements, coords=coords, cartesian=False, ) elif cartn_x_index != "": for ii, i in enumerate(atom_liines): tmp = i.split() tmp_lbl = list( Composition.from_string(tmp[label_index]).to_dict().keys() ) elem = tmp_lbl[0] coord = [ float(tmp[cartn_x_index].split("(")[0]), float(tmp[cartn_y_index].split("(")[0]), float(tmp[cartn_z_index].split("(")[0]), ] if len(tmp_lbl) > 1: raise ValueError("Check if labesl are correct.", tmp_lbl) if ( occupancy_index != "" and not float( tmp[occupancy_index].split("(")[0] ).is_integer() ): raise ValueError( "Fractional occupancy is not supported.", float(tmp[occupancy_index].split("(")[0]), elem, ) elements.append(elem) coords.append(coord) cif_atoms = Atoms( lattice_mat=lat.matrix, elements=elements, coords=coords, cartesian=True, ) else: raise ValueError( "Cannot find atomic coordinate info from cart or frac." ) # frac_coords=list(cif_atoms.frac_coords) cif_elements = cif_atoms.elements lat = cif_atoms.lattice.matrix if len(symm_ops) > 1: frac_coords = list(cif_atoms.frac_coords) for i in symm_ops: for jj, j in enumerate(frac_coords): new_c_coord = get_new_coord_for_xyz_sym( xyz_string=i, frac_coord=j ) new_frac_coord = [new_c_coord][0] if not check_duplicate_coords(frac_coords, new_frac_coord): frac_coords.append(new_frac_coord) cif_elements.append(cif_elements[jj]) new_atoms = Atoms( lattice_mat=lat, coords=frac_coords, elements=cif_elements, cartesian=False, ) cif_atoms = new_atoms return cif_atoms