def reorient(structure, miller_index, rotate=0.): # Align miller_index direction to z-direction [0,0,1] struct = structure.copy() latt = struct.lattice recp = latt.reciprocal_lattice_crystallographic normal = recp.get_cartesian_coords(miller_index) normal /= np.linalg.norm(normal) z = [0, 0, 1] rot_axis = np.cross(normal, z) # Check if normal and z are linearly dependent if not np.isclose(rot_axis, [0, 0, 0]).all(): angle = np.arccos(np.clip(np.dot(normal, z), -1.0, 1.0)) struct = RotationTransformation( rot_axis, math.degrees(angle)).apply_transformation(struct) # Align other axis (longest) to x-axis lattm = struct.lattice.matrix basis_lengths_xy = [ lattm[0][0]**2 + lattm[0][1]**2, lattm[1][0]**2 + lattm[1][1]**2, lattm[2][0]**2 + lattm[2][1]**2 ] max_ind = basis_lengths_xy.index(max(basis_lengths_xy)) max_basis = list(lattm[max_ind]) max_basis[2] = 0 max_basis /= np.linalg.norm(max_basis) angle2 = np.arccos(np.clip(np.dot(max_basis, [1, 0, 0]), -1.0, 1.0)) struct = RotationTransformation( z, math.degrees(angle2)).apply_transformation(struct) # Check if correct sign of rotation was applied, if not: rotate twice the angle the other direction if abs(struct.lattice.matrix[max_ind][1]) > 1e-5: struct = RotationTransformation( z, -2 * math.degrees(angle2)).apply_transformation(struct) if rotate: struct = RotationTransformation(z, rotate).apply_transformation(struct) return struct
def test_get_symmetry_hash(): """Test the symmetry hashes""" zif3 = MOFChecker.from_cif( os.path.join(THIS_DIR, "test_files", "ZIF-3.cif")) print(type(zif3.structure)) hash_a = get_symmetry_hash(zif3.structure, tight=True) assert len(hash_a) == 275 hash_b = get_symmetry_hash(zif3.structure) assert len(hash_b) == 47 assert hash_a[-3:] == hash_b[-3:] zif4 = MOFChecker.from_cif( os.path.join(THIS_DIR, "test_files", "ZIF-4.cif")) hash_zif4_a = get_symmetry_hash(zif4.structure, tight=True) hash_zif4_b = get_symmetry_hash(zif4.structure) assert hash_zif4_a != hash_a assert hash_zif4_b != hash_b assert zif4.symmetry_hash == hash_zif4_b structure = Structure.from_file( os.path.join(THIS_DIR, "test_files", "ABAXUZ.cif")) original_hash = get_symmetry_hash(MOFChecker(structure).structure) # rotate structure rotation_transformer = RotationTransformation([1, 0, 0], 10) rotated_structure = rotation_transformer.apply_transformation(structure) assert get_symmetry_hash( MOFChecker(rotated_structure).structure) == original_hash # create supercell structure.make_supercell([1, 2, 1]) print(type(structure)) assert get_symmetry_hash(MOFChecker(structure).structure) == original_hash
def rotate_c_parallel_to_z(self): """ Rotates the cell such that lattice vector c is parallel to the Cartesian z-axis. Note: this method doesn't change the fractional coordinates of the sites. However, the Cartesian coordinates may be changed. """ # rotate about the z-axis until c lies in the x-z plane rotation = RotationTransformation( [0, 0, 1], 180 - (180 / np.pi) * np.arctan2(self.lattice.matrix[2][1], self.lattice.matrix[2][0])) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) # rotate about the y-axis to make c parallel to the z-axis rotation = RotationTransformation( [0, 1, 0], 180 - (180 / np.pi) * np.arctan2(self.lattice.matrix[2][0], self.lattice.matrix[2][2])) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) # make sure c is pointing along the positive z-axis if self.lattice.matrix[2][2] < 0: # rotate 180 degrees about the x-axis rotation = RotationTransformation([1, 0, 0], 180) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice)
def create_sample(edc, structure, angle_start, angle_change): dps = [] for orientation in create_pair(angle_start, angle_change): axis, angle = euler2axangle(orientation[0], orientation[1], orientation[2], 'rzxz') rotation = RotationTransformation(axis, angle, angle_in_radians=True) rotated_structure = rotation.apply_transformation(structure) data = edc.calculate_ed_data( rotated_structure, reciprocal_radius=0.9, #avoiding a reflection issue with_direct_beam=False) dps.append(data.as_signal(2 * half_side_length, 0.025, 1).data) dp = pxm.ElectronDiffraction([dps[0:2], dps[2:]]) dp.set_diffraction_calibration(1 / half_side_length) return dp
def planar_structure_normalization(structure): ''' This function does the following: 1. check whether the structure is planar using coordniates standard deviation 2. move the planar layer to the center of c-direction Args: structure: pymatgen structure Return: a boolean whether the structure is planar tranformed pymatgen structure ''' tol = 1E-3 # tolerance to check whether the structure is planar is_planar = True coords = structure.frac_coords ts = TransformedStructure(structure, []) if np.std(coords[:,2]) < tol : center_translate = 0.5 - coords[:,2].mean() elif np.std(coords[:,0]) < tol : ts.append_transformation(SupercellTransformation([[0,0,1],[0,1,0],[1,0,0]])) ts.append_transformation(RotationTransformation([0,1,0], 90)) center_translate = 0.5 - coords[:,0].mean() elif np.std(coords[:,1]) < tol : ts.append_transformation(SupercellTransformation([[1,0,0],[0,0,1],[0,1,0]])) ts.append_transformation(RotationTransformation([1,0,0], 90)) center_translate = 0.5 - coords[:,1].mean() else : is_planar = False transformed_structure = None if is_planar: ts.append_transformation(TranslateSitesTransformation( list(range(len(structure))), [0,0,center_translate])) # Use pymatgen 2019.7.2, ts.structures[-1] may change in a newer version transformed_structure = ts.structures[-1] return is_planar, transformed_structure
def get_diffraction_library(self, structure_library, calibration, reciprocal_radius, representation='euler'): """Calculates a dictionary of diffraction data for a library of crystal structures and orientations. Each structure in the structure library is rotated to each associated orientation and the diffraction pattern is calculated each time. Parameters ---------- structure_library : dict Dictionary of structures and associated orientations (represented as Euler angles or axis-angle pairs) for which electron diffraction is to be simulated. calibration : float The calibration of experimental data to be correlated with the library, in reciprocal Angstroms per pixel. reciprocal_radius : float The maximum g-vector magnitude to be included in the simulations. representation : 'euler' or 'axis-angle' The representation in which the orientations are provided. If 'euler' the zxz convention is taken and values are in radians, if 'axis-angle' the rotational angle is in degrees. Returns ------- diffraction_library : dict of :class:`DiffractionSimulation` Mapping of crystal structure and orientation to diffraction data objects. """ # Define DiffractionLibrary object to contain results diffraction_library = DiffractionLibrary() # The electron diffraction calculator to do simulations diffractor = self.electron_diffraction_calculator # Iterate through phases in library. for key in structure_library.keys(): phase_diffraction_library = dict() structure = structure_library[key][0] orientations = structure_library[key][1] # Iterate through orientations of each phase. for orientation in tqdm(orientations, leave=False): if representation=='axis-angle': axis = [orientation[0], orientation[1], orientation[2]] angle = orientation[3] / 180 * pi if representation=='euler': axis, angle = euler2axangle(orientation[0], orientation[1], orientation[2], 'rzxz') # Apply rotation to the structure rotation = RotationTransformation(axis, angle, angle_in_radians=True) rotated_structure = rotation.apply_transformation(structure) # Calculate electron diffraction for rotated structure data = diffractor.calculate_ed_data(rotated_structure, reciprocal_radius) # Calibrate simulation data.calibration = calibration # Construct diffraction simulation library. phase_diffraction_library[tuple(orientation)] = data diffraction_library[key] = phase_diffraction_library return diffraction_library
def rotate_to_principal_directions(self): """ Rotates the cell into the principal directions. That is, lattice vector a is parallel to the Cartesian x-axis, lattice vector b lies in the Cartesian x-y plane and the z-component of lattice vector c is positive. Note: this method doesn't change the fractional coordinates of the sites. However, the Cartesian coordinates may be changed. """ # rotate about the z-axis to align a vertically with the x-axis rotation = RotationTransformation( [0, 0, 1], 180 - (180 / np.pi) * np.arctan2(self.lattice.matrix[0][1], self.lattice.matrix[0][0])) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) # rotate about the y-axis to make a parallel to the x-axis rotation = RotationTransformation( [0, 1, 0], (180 / np.pi) * np.arctan2(self.lattice.matrix[0][2], self.lattice.matrix[0][0])) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) # rotate about the x-axis to make b lie in the x-y plane rotation = RotationTransformation( [1, 0, 0], 180 - (180 / np.pi) * np.arctan2(self.lattice.matrix[1][2], self.lattice.matrix[1][1])) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) # make sure they are all pointing in positive directions if self.lattice.matrix[0][0] < 0: # rotate about y-axis to make a positive rotation = RotationTransformation([0, 1, 0], 180) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) if self.lattice.matrix[1][1] < 0: # rotate about x-axis to make b positive rotation = RotationTransformation([1, 0, 0], 180) new_structure = rotation.apply_transformation(self) self.modify_lattice(new_structure.lattice) if self.lattice.matrix[2][2] < 0: # mirror c across the x-y plane to make it positive # a and b a = self.lattice.matrix[0] b = self.lattice.matrix[1] # the components of c cx = self.lattice.matrix[2][0] cy = self.lattice.matrix[2][1] cz = -1 * self.lattice.matrix[2][2] self.modify_lattice(Lattice([a, b, [cx, cy, cz]]))
def slab_has_mirror_sym(slab, nterm=1, tol=0.01): '''This function tests if the input slab has a mirror symmetry in the c-direction (given tolerance 'tol' in Angstrom). Specify 'nterm' (number of unique terminations) to ensure enough subtractions from the top/bottom of the layer are considered. Unit cell can be of any shape, however make sure that the layer thickness in c-direction is thick enough (recommendation: SlabGenerator with min_slab_size=4, in_unit_planes=True) Remark: A bug in pymatgen/transformations/standard_transformations.py was fixed in version pymatgen-2020.4.29 Output: [mirror_sym, error] where 'mirror_sym' is the mirror symmetry (True or False), 'error' may contain an error message in case any step of the unit cell rotation didn't work, otherwise contains empty string Procedure: 1. Rotate unit cell such that Cartesian coordinates of a and b basis vectors is of form (a_x,0,0) and (b_x,b_y,0), respectively. 2. Create new c-direction orthogonal to a and b. 3. Add atoms from slab in step 1 to new unit cell of step 2. 4. Use function mirror_sym_check to check symmetry for the initial slab, the slab with the topmost, the slab with lowermost layer missing, and the slab with both topmost/lowermost layers missing. If nterm > 3, additionally np.floor(nterm/2) layers are subtracted from either side and their symmetry checked. ''' error = "" if round(slab.lattice.alpha, 1) == 90.0 and round(slab.lattice.beta, 1) == 90.0: slab_straight = Structure(slab._lattice, slab.species_and_occu, slab.frac_coords) else: R = slab.lattice.matrix #print(R) #if a base vector not parallel to x-axis in caartesian coords, rotate the cell/structure such that it is if abs(R[0, 1]) > 0.0001 or abs(R[0, 2]) > 0.0001: x = [1, 0, 0] rot_axis = np.cross(R[0], x) angle = np.arccos( np.clip( np.dot(R[0] / np.linalg.norm(R[0]), x / np.linalg.norm(x)), -1.0, 1.0)) slab = RotationTransformation( rot_axis, math.degrees(angle)).apply_transformation(slab) R = slab.lattice.matrix #In case the wrong sign of the rotation was applied, rotate back twice the angle: if abs(R[0, 1]) > 0.0001 or abs(R[0, 2]) > 0.0001: slab = RotationTransformation( rot_axis, -2 * math.degrees(angle)).apply_transformation(slab) R = slab.lattice.matrix if abs(R[0, 1]) > 0.0001 or abs(R[0, 2]) > 0.0001: error = "Error. Could not rotate a-axis to be parallel to x-axis." #print(R) #if b vector not lying in cartesian x-y plane, rotate to make it so (i.e. z-component of b vector = 0) if abs(R[1, 2]) > 0.0001 and not error: b_x_flat = [0, 0, 0] b_x_flat[1] = R[1, 1] b_x_flat[2] = R[1, 2] x = [1, 0, 0] y = [0, 1, 0] angle2 = np.arccos( np.clip( np.dot(b_x_flat / np.linalg.norm(b_x_flat), y / np.linalg.norm(y)), -1.0, 1.0) ) #angle between y-axis and b vector projected onto y-z-plane slab = RotationTransformation( x, math.degrees(angle2)).apply_transformation(slab) R = slab.lattice.matrix #In case the wrong sign of the rotation was applied, rotate back twice the angle: if abs(R[1, 2]) > 0.0001: slab = RotationTransformation( x, -2 * math.degrees(angle2)).apply_transformation(slab) R = slab.lattice.matrix if abs(R[1, 2]) > 0.0001: error = "Error. Could not rotate b-vector to lie within x-y-plane." if R[1, 1] < -0.0001: error = "Error. Vector b faces into negative y-direction (which could cause problems)." #Now create new c-direction that is orthogonal to the rotated a and b vectors if not error: N = np.array(R) #new lattice vectors N[2] = np.cross(R[0], R[1]) N[2] = N[2] * np.dot( R[2], N[2]) / (np.linalg.norm(N[2]) * np.linalg.norm(N[2])) latticeN = Lattice(N) #new lattice with c perpendicular to a,b #Add atoms from rotated unit cell to new unit cell with orthogonal c-direction slab_straight = Structure(latticeN, slab.species, slab.cart_coords, coords_are_cartesian=True) #Check mirror symmetry for straightened slab as well as slabs that are missing either (or both) topmost, lowermost atom-layers if not error: mirror_sym = False #Checks mirror symmetry of slab_straight without any layers removed slab_orig = Structure(slab_straight._lattice, slab_straight.species_and_occu, slab_straight.frac_coords) msym, err = mirror_sym_check(slab_orig, tol=tol) if msym: mirror_sym = True if not err == '': error = err #Checks mirror symmetry of slab_straight with lowermost layer removed min_value = np.min(slab_orig.cart_coords[:, 2]) first_layer_size = len( np.where(np.abs(min_value - slab_orig.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmin(slab_orig.frac_coords[:, 2]) slab_orig.remove_sites([index_to_remove]) msym, err = mirror_sym_check(slab_orig, tol=tol) if msym: mirror_sym = True if not err == '': error = err #Checks mirror symmetry of slab_straight with topmost layer removed max_value = np.max(slab_straight.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(max_value - slab_straight.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmax(slab_straight.frac_coords[:, 2]) slab_straight.remove_sites([index_to_remove]) msym, err = mirror_sym_check(slab_straight, tol=tol) if msym: mirror_sym = True if not err == '': error = err #Checks mirror symmetry of slab_straight with lowermost and topmost layers removed min_value = np.min(slab_straight.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(min_value - slab_straight.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmin(slab_straight.frac_coords[:, 2]) slab_straight.remove_sites([index_to_remove]) msym, err = mirror_sym_check(slab_straight, tol=tol) if msym: mirror_sym = True if not err == '': error = err #If nterm is larger than 3, remove additonal layers from either side to ensure #that we check all relevant slabs that could yield proof that there is mirror symmetry if nterm > 3: slab_orig = Structure(slab_straight._lattice, slab_straight.species_and_occu, slab_straight.frac_coords) #delete more layers nterm/2 rounded down on either side from the slab_straight (which had one each side already subtracted) for term in range(int(np.floor(nterm / 2))): max_value = np.max(slab_straight.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(max_value - slab_straight.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmax(slab_straight.frac_coords[:, 2]) slab_straight.remove_sites([index_to_remove]) msym, err = mirror_sym_check(slab_straight, tol=tol) if msym: mirror_sym = True if not err == '': error = err min_value = np.min(slab_orig.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(min_value - slab_orig.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmin(slab_orig.frac_coords[:, 2]) slab_orig.remove_sites([index_to_remove]) msym, err = mirror_sym_check(slab_orig, tol=tol) if msym: mirror_sym = True if not err == '': error = err else: #Mirror symmetry is set to False in case there was an error along the way. mirror_sym = False return mirror_sym, error
def test_rotation_transformation(self): t = RotationTransformation([0, 1, 0], 30, False) s2 = t.apply_transformation(self.struct) s1 = t.inverse.apply_transformation(s2) self.assertTrue( (abs(s1.lattice.matrix - self.struct.lattice.matrix) < 1e-8).all())
def test_as_from_dict(self): t = RotationTransformation([0, 1, 0], 30, False) d = t.as_dict() self.assertEqual(type(RotationTransformation.from_dict(d)), RotationTransformation)
def get_diffraction_library(self, structure_library, calibration, reciprocal_radius, half_shape, representation='euler', with_direct_beam=True): """Calculates a dictionary of diffraction data for a library of crystal structures and orientations. Each structure in the structure library is rotated to each associated orientation and the diffraction pattern is calculated each time. Parameters ---------- structure_library : dict Dictionary of structures and associated orientations (represented as Euler angles or axis-angle pairs) for which electron diffraction is to be simulated. calibration : float The calibration of experimental data to be correlated with the library, in reciprocal Angstroms per pixel. reciprocal_radius : float The maximum g-vector magnitude to be included in the simulations. representation : 'euler' or 'axis-angle' The representation in which the orientations are provided. If 'euler' the zxz convention is taken and values are in radians, if 'axis-angle' the rotational angle is in degrees. half_shape: tuple The half shape of the target patterns, for 144x144 use (72,72) etc Returns ------- diffraction_library : dict of :class:`DiffractionSimulation` Mapping of crystal structure and orientation to diffraction data objects. """ # Define DiffractionLibrary object to contain results diffraction_library = DiffractionLibrary() # The electron diffraction calculator to do simulations diffractor = self.electron_diffraction_calculator # Iterate through phases in library. for key in structure_library.keys(): phase_diffraction_library = dict() structure = structure_library[key][0] orientations = structure_library[key][1] # Iterate through orientations of each phase. for orientation in tqdm(orientations, leave=False): if representation == 'axis-angle': axis = [orientation[0], orientation[1], orientation[2]] angle = orientation[3] / 180 * pi if representation == 'euler': axis, angle = euler2axangle(orientation[0], orientation[1], orientation[2], 'rzxz') # Apply rotation to the structure rotation = RotationTransformation(axis, angle, angle_in_radians=True) rotated_structure = rotation.apply_transformation(structure) # Calculate electron diffraction for rotated structure data = diffractor.calculate_ed_data(rotated_structure, reciprocal_radius, with_direct_beam) # Calibrate simulation data.calibration = calibration pattern_intensities = data.intensities pixel_coordinates = np.rint( data.calibrated_coordinates[:, :2] + half_shape).astype(int) # Construct diffraction simulation library, removing those that contain no peaks if len(pattern_intensities) > 0: phase_diffraction_library[tuple(orientation)] = \ {'Sim':data,'intensities':pattern_intensities, \ 'pixel_coords':pixel_coordinates, \ 'pattern_norm': np.sqrt(np.dot(pattern_intensities,pattern_intensities))} diffraction_library[key] = phase_diffraction_library return diffraction_library
def test_graph_hash_robustness(): # pylint: disable=too-many-locals """Check that duplicating or rotating the structure produces the same hash.""" structure = Structure.from_file( os.path.join(THIS_DIR, "test_files", "ABAXUZ.cif")) original_hash = MOFChecker(structure).graph_hash # rotate structure rotation_transformer = RotationTransformation([1, 0, 0], 10) rotated_structure = rotation_transformer.apply_transformation(structure) assert MOFChecker(rotated_structure).graph_hash == original_hash # create supercell structure.make_supercell([1, 2, 1]) assert MOFChecker(structure).graph_hash == original_hash # check the MOF-74 structures mohgoi_checker = MOFChecker( Structure.from_file(os.path.join(THIS_DIR, "test_files", "MOHGOI.cif"))) todyuj_checker = MOFChecker( Structure.from_file(os.path.join(THIS_DIR, "test_files", "TODYUJ.cif"))) vogtiv_checker = MOFChecker( Structure.from_file(os.path.join(THIS_DIR, "test_files", "VOGTIV.cif"))) # There water on TODYUJ assert mohgoi_checker.graph_hash != todyuj_checker.graph_hash # one is the supercell of the other assert mohgoi_checker.graph_hash == vogtiv_checker.graph_hash # MOF-74-Zr.cif mof_74_zr = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "MOF-74-Zr.cif"))) assert mof_74_zr.graph_hash != todyuj_checker.graph_hash assert mof_74_zr.graph_hash != vogtiv_checker.graph_hash # # MOF-74-Zn.cif mof_74_zn = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "MOF-74-Zn.cif"))) assert mof_74_zr.scaffold_hash == mof_74_zn.scaffold_hash assert mof_74_zr.graph_hash != mof_74_zn.graph_hash # # MOF-5 is not ZIF-8 mof_5 = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "mof-5_cellopt.cif"))) zif_8 = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "ZIF-8-RASPA.cif"))) assert mof_5.graph_hash != zif_8.graph_hash # # Mn-MOF-74 and UiO-67 coknun = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "coknun01.cif"))) wizmac = MOFChecker( Structure.from_file( os.path.join(THIS_DIR, "test_files", "WIZMAV02_auto.cif"))) assert coknun.graph_hash != wizmac.graph_hash