def search_position(self): """ Sometimes, the initial posiition is not the proper generator Needs to find the proper generator """ if self.wp.index > 0: pos = self.position coords = apply_ops(pos, Group(self.wp.number, self.wp.dim)[0]) for coord in coords: ans = apply_ops(coord, [self.wp.ops[0]])[0] diff = coord - ans diff -= np.floor(diff) if np.sum(diff**2) < 1e-4: self.position = coord - np.floor(coord) break
def subgroup_by_splitter(self, splitter, eps=0.05): lat1 = np.dot(splitter.R[:3, :3].T, self.lattice.matrix) multiples = np.linalg.det(splitter.R[:3, :3]) split_sites = [] for i, site in enumerate(self.atom_sites): pos = site.position for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): pos0 = apply_ops(pos, ops1)[0] pos0 -= np.floor(pos0) pos0 += eps * (np.random.sample(3) - 0.5) wp, _ = Wyckoff_position.from_symops(ops2, group=splitter.H.number, permutation=False) split_sites.append(atom_site(wp, pos0, site.specie)) new_struc = deepcopy(self) new_struc.group = splitter.H lattice = Lattice.from_matrix(lat1, ltype=new_struc.group.lattice_type) new_struc.lattice = lattice.mutate(degree=0.01, frozen=True) new_struc.atom_sites = split_sites new_struc.numIons = [ int(multiples * numIon) for numIon in self.numIons ] new_struc.source = 'Wyckoff Split' return new_struc
def update(self, pos=None): """ Used to generate coords from self.position """ if pos is None: pos = self.position self.coords = apply_ops(pos, self.wp) self.position = self.coords[0]
def _find_gen_wyckoff_in_subgroup(self, groups=None): """ Symmetry transformation group -> subgroup At the moment we only consider for multiplicity 2: P-1, P21, P2, Pm and Pc to add: for multiplicity 4: P21/c, P212121 Permutation is allowed """ from pyxtal.symmetry import Wyckoff_position pos = self.position wp0 = self.wp pos0 = apply_ops(pos, wp0) if len(wp0) == 2: if self.diag: # P21/n -> Pn #print("----------P21n----------") wp1 = Wyckoff_position.from_group_and_index(7, 0) wp1.diagonalize_symops() axes = [[0, 1, 2], [2, 1, 0]] for ax in axes: pos1 = apply_ops(pos[ax], wp1) diff = (pos1[:, ax] - pos0)[1] diff -= np.floor(diff) if len(diff[diff == 0]) >= 2: #return wp1, ax, pos[ax] - 0.5*diff return Wyckoff_position.from_group_and_index( 7, 0), ax, pos[ax] - 0.5 * diff else: if groups is None: groups = [4, 3, 6, 7, 2] if 15 < wp0.number < 71: axes = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [2, 1, 0]] elif wp0.number < 15: axes = [[0, 1, 2], [2, 1, 0]] for group in groups: wp1 = Wyckoff_position.from_group_and_index(group, 0) for ax in axes: pos1 = apply_ops(pos[ax], wp1) diff = (pos1[:, ax] - pos0)[1] diff -= np.floor(diff) if len(diff[diff == 0]) >= 2: return wp1, ax, pos[ax] - 0.5 * diff
def get_centers(self, absolute=False): """ Returns the fractional coordinates for the center of mass for each molecule in the Wyckoff position Returns: A numpy array of fractional 3-vectors """ centers = apply_ops(self.position, self.wp.ops) # centers1 = filtered_coords(centers0, self.PBC) if absolute is False: return centers else: return np.dot(centers, self.lattice.matrix)
def subgroup_by_splitter(self, splitter, eps=0.05): """ transform the crystal to subgroup symmetry from a splitter object """ lat1 = np.dot(splitter.R[:3, :3].T, self.lattice.matrix) multiples = np.linalg.det(splitter.R[:3, :3]) new_struc = deepcopy(self) new_struc.group = splitter.H lattice = Lattice.from_matrix(lat1, ltype=new_struc.group.lattice_type) lattice = lattice.mutate(degree=eps, frozen=True) h = splitter.H.number split_sites = [] if self.molecular: # below only works when the cell does not change for i, site in enumerate(self.mol_sites): pos = site.position mol = site.molecule ori = site.orientation coord0 = mol.mol.cart_coords.dot(ori.matrix.T) wp1 = site.wp ori.reset_matrix(np.eye(3)) for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): #reset molecule coord1 = np.dot(coord0, ops1[0].affine_matrix[:3, :3].T) _mol = mol.copy() _mol.reset_positions(coord1) pos0 = apply_ops(pos, ops1)[0] pos0 -= np.floor(pos0) pos0 += eps * (np.random.sample(3) - 0.5) wp, _ = Wyckoff_position.from_symops(ops2, h, permutation=False) split_sites.append(mol_site(_mol, pos0, ori, wp, lattice)) new_struc.mol_sites = split_sites new_struc.numMols = [ int(multiples * numMol) for numMol in self.numMols ] else: for i, site in enumerate(self.atom_sites): pos = site.position for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): pos0 = apply_ops(pos, ops1)[0] pos0 -= np.floor(pos0) pos0 += eps * (np.random.sample(3) - 0.5) wp, _ = Wyckoff_position.from_symops(ops2, h, permutation=False) split_sites.append(atom_site(wp, pos0, site.specie)) new_struc.atom_sites = split_sites new_struc.numIons = [ int(multiples * numIon) for numIon in self.numIons ] new_struc.lattice = lattice new_struc.source = 'Wyckoff Split' return new_struc
def symmetrize(self, splitter, solution, disp): """ For a given solution, search for the possbile supergroup structure Args: splitter: splitter object to specify the relation between G and H disp: an overall shift from H to G, None or 3 vector d_tol: the tolerance in angstrom Returns: coords_G1 coords_G2 coords_H1 elements mults """ atom_sites_H = self.struc.atom_sites coords_G1 = [] # position in G coords_G2 = [] # position in G on the subgroup bais coords_H1 = [] # position in H elements = [] mults = [] inv_R = splitter.inv_R # inverse coordinate transformation # wp1 stores the wyckoff position object of ['2c', '6h', '12i'] for i, wp1 in enumerate(splitter.wp1_lists): if len(splitter.wp2_lists[i]) == 1: # symmetry info ops_H = splitter.H_orbits[i][0] # ops for H ops_G2 = splitter.G2_orbits[i][0] # ops for G2 # refine coord1 to find the best match on coord2 coord = atom_sites_H[solution[i][0]].position coord0s = apply_ops(coord, ops_H) # possible coords in H dists = [] for coord0 in coord0s: coord2 = coord0.copy() coord2 += disp coord1 = apply_ops(coord2, ops_G2)[0] # coord in G dist = coord1 - coord2 dist -= np.round(dist) dist = np.dot(dist, self.cell) dists.append(np.linalg.norm(dist)) min_ID = np.argmin(np.array(dists)) coord2 = coord0s[min_ID].copy() coord1 = ops_G2[0].operate(coord2 + disp) if round(np.trace(ops_G2[0].rotation_matrix)) in [1, 2]: def fun(x, pt, ref, op): pt[0] = x[0] y = op.operate(pt) diff = y - ref diff -= np.round(diff) diff = np.dot(diff, self.cell) return np.linalg.norm(diff) # optimize the distance by changing coord1 res = minimize(fun, coord1[0], args=(coord1, coord2, ops_G2[0]), method='Nelder-Mead', options={'maxiter': 20}) coord1[0] = res.x[0] coord1 = ops_G2[0].operate(coord1) coords_G1.append( np.dot(inv_R[:3, :3], coord1).T + inv_R[:3, 3].T) #coords_G1.append(coord1) coords_G2.append(coord1) coords_H1.append(coord2) elements.append(splitter.elements[i]) mults.append(len(ops_G2)) else: # symmetry operations ops_H1 = splitter.H_orbits[i][0] ops_G22 = splitter.G2_orbits[i][1] coord1 = atom_sites_H[solution[i][0]].position.copy() coord2 = atom_sites_H[solution[i][1]].position.copy() # refine coord1 to find the best match on coord2 coord11 = coord1 + disp coord22 = coord2 + disp coords11 = apply_ops(coord11, ops_H1) # transform coords1 by symmetry operation op = ops_G22[0] for m, coord11 in enumerate(coords11): coords11[m] = op.operate(coord11) tmp, dist = get_best_match(coords11, coord22, self.cell) # recover the original position inv_op = get_inverse(op) coord1 = inv_op.operate(tmp) coord1 -= disp d = coord22 - tmp d -= np.round(d) coord22 -= d / 2 #final coord2 after disp # recover the displaced position coord11 = inv_op.operate(coord22) coords_G1.append( np.dot(inv_R[:3, :3], coord11).T + inv_R[:3, 3].T) coords_G2.append(coord11) coords_G2.append(coord22) coords_H1.append(coord1) coords_H1.append(coord2) elements.extend([splitter.elements[i]] * 2) mults.append(len(ops_H1)) mults.append(len(ops_G22)) return coords_G1, coords_G2, coords_H1, elements, mults
def symmetrize_dist(self, splitter, solution, disp=None, mask=None, d_tol=1.2): """ For a given solution, search for the possbile supergroup structure Args: splitter: splitter object to specify the relation between G and H solution: list of sites in H, e.g., ['4a', '8b'] disp: an overall shift from H to G, None or 3 vector d_tol: the tolerance in angstrom Returns: distortion cell translation """ max_disps = [] atom_sites_H = self.struc.atom_sites n_atoms = sum([site.wp.multiplicity for site in atom_sites_H]) #print("checking solution-----------------", solution) # wp1 stores the wyckoff position object of ['2c', '6h', '12i'] for i, wp1 in enumerate(splitter.wp1_lists): if len(splitter.wp2_lists[i]) == 1: # one to one splitting, e.g., 2c->2d # this usually involves increase of site symmetry # symmetry info ops_H = splitter.H_orbits[i][0] # ops for H ops_G2 = splitter.G2_orbits[i][0] # ops for G2 # refine coord1 to find the best match on coord2 coord = atom_sites_H[solution[i][0]].position coord0s = apply_ops(coord, ops_H) # possible coords in H dists = [] for coord0 in coord0s: coord2 = coord0.copy() if disp is not None: coord2 += disp coord1 = apply_ops(coord2, ops_G2)[0] # coord in G #print(coord1, coord2) dist = coord1 - coord2 dist -= np.round(dist) dist = np.dot(dist, self.cell) dists.append(np.linalg.norm(dist)) min_ID = np.argmin(np.array(dists)) dist = dists[min_ID] coord2 = coord0s[min_ID].copy() #print("---------", wp1.letter, coord1, coord2, disp, dist) #print(splitter) #print(splitter.R) if disp is None: coord1 = apply_ops(coord2, ops_G2)[0] disp = (coord1 - coord2).copy() # temporary fix xyz1 = ops_H[0].as_xyz_string().split(',') xyz2 = ops_G2[0].as_xyz_string().split(',') mask = [m for m in range(3) if xyz1[m] == xyz2[m]] #disp[mask] = 0 #if abs(disp[0] + disp[1]) < 1e-2: # disp[:2] = 0 #print(disp, coord1, coord2) elif dist < d_tol: coord1 = ops_G2[0].operate(coord2 + disp) if round(np.trace(ops_G2[0].rotation_matrix)) in [1, 2]: def fun(x, pt, ref, op): pt[0] = x[0] y = op.operate(pt) diff = y - ref diff -= np.round(diff) diff = np.dot(diff, self.cell) return np.linalg.norm(diff) # optimize the distance by changing coord1 res = minimize(fun, coord1[0], args=(coord1, coord2, ops_G2[0]), method='Nelder-Mead', options={'maxiter': 20}) coord1[0] = res.x[0] coord1 = ops_G2[0].operate(coord1) else: return 10000, None, None if mask is not None: disp[mask] = 0 diff = coord1 - (coord2 + disp) diff -= np.round(diff) max_disps.append(np.linalg.norm(np.dot(diff, self.cell))) else: # symmetry operations ops_H1 = splitter.H_orbits[i][0] ops_G22 = splitter.G2_orbits[i][1] coord1 = atom_sites_H[solution[i][0]].position.copy() coord2 = atom_sites_H[solution[i][1]].position.copy() # refine coord1 to find the best match on coord2 if disp is not None: coord11 = coord1 + disp coord22 = coord2 + disp else: coord11 = coord1 coord22 = coord2 coords11 = apply_ops(coord11, ops_H1) # transform coords1 by symmetry operation op = ops_G22[0] for m, coord11 in enumerate(coords11): coords11[m] = op.operate(coord11) tmp, dist = get_best_match(coords11, coord22, self.cell) if dist > np.sqrt(2) * d_tol: return 10000, None, mask # recover the original position try: inv_op = get_inverse(op) except: print("Error in getting the inverse") print(op) print(op.as_xyz_string()) import sys sys.exit() coord1 = inv_op.operate(tmp) if disp is not None: coord1 -= disp d = coord22 - tmp d -= np.round(d) coord22 -= d / 2 #final coord2 after disp # recover the displaced position coord11 = inv_op.operate(coord22) max_disps.append(np.linalg.norm(np.dot(d / 2, self.cell))) return max(max_disps), disp, mask
def _subgroup_by_splitter(self, splitter, eps=0.05, mut_lat=True): """ transform the crystal to subgroup symmetry from a splitter object Args: splitter: wyckoff splitter object eps (float): maximum atomic displacement in Angstrom mut_lat (bool): whether or not mutate the lattice """ lat1 = np.dot(splitter.R[:3,:3].T, self.lattice.matrix) multiples = np.linalg.det(splitter.R[:3,:3]) new_struc = self.copy() new_struc.group = splitter.H lattice = Lattice.from_matrix(lat1, ltype=new_struc.group.lattice_type) if mut_lat: lattice=lattice.mutate(degree=eps, frozen=True) h = splitter.H.number split_sites = [] if self.molecular: # below only works when the cell does not change for i, site in enumerate(self.mol_sites): pos = site.position mol = site.molecule ori = site.orientation coord0 = mol.mol.cart_coords.dot(ori.matrix.T) coord0 = np.dot(coord0, splitter.R[:3,:3]) wp1 = site.wp ori.reset_matrix(np.eye(3)) id = 0 for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): #reset molecule rot = wp1.generators_m[id].affine_matrix[:3,:3].T coord1 = np.dot(coord0, rot) _mol = mol.copy() _mol.reset_positions(coord1) pos0 = apply_ops(pos, ops1)[0] pos0 -= np.floor(pos0) dis = (np.random.sample(3) - 0.5).dot(self.lattice.matrix) dis /= np.linalg.norm(dis) pos0 += eps*dis*(np.random.random()-0.5) wp, _ = Wyckoff_position.from_symops(ops2, h, permutation=False) if h in [7, 14] and self.group.number == 31: diag = True else: diag = self.diag split_sites.append(mol_site(_mol, pos0, ori, wp, lattice, diag)) id += wp.multiplicity new_struc.mol_sites = split_sites new_struc.numMols = [int(multiples*numMol) for numMol in self.numMols] else: for i, site in enumerate(self.atom_sites): pos = site.position for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): pos0 = apply_ops(pos, ops1)[0] pos0 -= np.floor(pos0) dis = (np.random.sample(3) - 0.5).dot(self.lattice.matrix) dis /= np.linalg.norm(dis) pos0 += np.dot(eps*dis*(np.random.random()-0.5), self.lattice.inv_matrix) wp, _ = Wyckoff_position.from_symops(ops2, h, permutation=False) split_sites.append(atom_site(wp, pos0, site.specie)) new_struc.atom_sites = split_sites new_struc.numIons = [int(multiples*numIon) for numIon in self.numIons] new_struc.lattice = lattice new_struc.source = 'subgroup' return new_struc
#sites = ['8a'] G, H, fac = 227, 166, 4 numIons = int(sum([int(i[:-1]) for i in sites]) / fac) C = random_crystal(G, ['C'], [numIons], sites=[sites]) spg1 = get_symmetry_dataset(C.to_ase(), symprec=1e-4)['international'] splitter = wyckoff_split(G=G, H=H, wp1=sites) print(splitter) lat1 = np.dot(C.lattice.matrix, splitter.R[:3, :3].T) pos1 = None for i, site in enumerate(C.atom_sites): pos = site.position for ops1, ops2 in zip(splitter.G2_orbits[i], splitter.H_orbits[i]): pos0 = pos + 0.05 * (np.random.sample(3) - 0.5) #print(pos0) pos_tmp = apply_ops(pos0, ops1) pos_tmp = apply_ops(pos_tmp[0], ops2) if pos1 is None: pos1 = pos_tmp else: pos1 = np.vstack((pos1, pos_tmp)) C1 = Atoms(["C"] * len(pos1), scaled_positions=pos1, cell=lat1, pbc=[1, 1, 1]) C1.write("1.vasp", format='vasp', vasp5=True, direct=True) C.to_ase().write("0.vasp", format='vasp', vasp5=True, direct=True) pmg_s1 = AseAtomsAdaptor.get_structure(C1) spg2 = get_symmetry_dataset(C1, symprec=1e-4)['international'] print(spg1, spg2, sm.StructureMatcher().fit(pmg_s1, C.to_pymatgen()))
def resort(self, molecules): from pyxtal.operations import apply_ops, find_ids # filter out the molecular generators lat = self.pmg_struc.lattice.matrix inv_lat = self.pmg_struc.lattice.inv_matrix new_lat = self.lattice.matrix positions = np.zeros([len(molecules),3]) for i in range(len(molecules)): positions[i, :] = np.dot(molecules[i].cart_coords.mean(axis=0), inv_lat) ids = [] #id for the generator mults = [] #the corresponding multiplicities visited_ids = [] for id, pos in enumerate(positions): if id not in visited_ids: ids.append(id) #print("pos", pos); print(self.wyc) centers = apply_ops(pos, self.wyc) tmp_ids = find_ids(centers, positions) visited_ids.extend(tmp_ids) mults.append(len(tmp_ids)) #print("check", id, tmp_ids) # add position and molecule # print("ids", ids, mults) self.numMols = [0] * len(self.ref_mols) self.positions = [] self.p_mols = [] self.wps = [] for i in range(len(ids)): mol1 = molecules[ids[i]] matched = False for j, mol2 in enumerate(self.ref_mols): match, mapping = compare_mol_connectivity(mol2.mol, mol1) if match: self.numMols[j] += mults[i] # rearrange the order order = [mapping[at] for at in range(len(mol1))] xyz = mol1.cart_coords[order] frac = np.dot(xyz, inv_lat) xyz = np.dot(frac, new_lat) # create p_mol p_mol = mol2.copy() center = p_mol.get_center(xyz) #print(xyz-center) p_mol.reset_positions(xyz-center) position = np.dot(center, np.linalg.inv(new_lat)) position -= np.floor(position) #print(position) #print(lat) #print(p_mol.mol.cart_coords[:10] + np.dot(position, new_lat)) # print(len(self.pmg_struc), len(self.molecule), len(self.wyc)) # check if molecule is on the special wyckoff position if mults[i] < len(self.wyc): #Transform it to the conventional representation if self.diag: position = np.dot(self.perm, position).T #print("molecule is on the special wyckoff position") position, wp, _ = WP_merge(position, new_lat, self.wyc, 0.1) self.wps.append(wp) #print("After Merge:---"); print(position); print(wp) else: self.wps.append(self.wyc) self.positions.append(position) self.p_mols.append(p_mol) matched = True break if not matched: print(mol1.to('xyz')) print(mol2.mol.to('xyz')) raise RuntimeError("molecule cannot be matched")
def WP_merge(pt, lattice, wp, tol, orientations=None): """ Given a list of fractional coordinates, merges them within a given tolerance, and checks if the merged coordinates satisfy a Wyckoff position. Used for merging general Wyckoff positions into special Wyckoff positions within the random_crystal (and its derivative) classes. Args: pt: the originl point (3-vector) lattice: a 3x3 matrix representing the unit cell wp: a `Wyckoff_position <pyxtal.symmetry.Wyckoff_position.html> object after merge tol: the cutoff distance for merging coordinates orientations: the valid orientations for a given molecule. Obtained from get_sg_orientations, which is called within molecular_crystal Returns: pt: 3-vector after merge wp: a `pyxtal.symmetry.Wyckoff_position` object, If no matching WP, returns False. valid_ori: the valid orientations after merge """ index = wp.index PBC = wp.PBC group = Group(wp.number, wp.dim) pt = project_point(pt, wp[0], lattice, PBC) coor = apply_ops(pt, wp) if orientations is None: valid_ori = None else: j, k = jk_from_i(index, orientations) valid_ori = orientations[j][k] # Main loop for merging multiple times while True: # Check distances of current WP. If too small, merge dm = distance_matrix([coor[0]], coor, lattice, PBC=PBC) passed_distance_check = True x = np.argwhere(dm < tol) for y in x: # Ignore distance from atom to itself if y[0] == 0 and y[1] == 0: pass else: passed_distance_check = False break # for molecular crystal, one more check if check_images([coor[0]], [6], lattice, PBC=PBC, tol=tol) is False: passed_distance_check = False if not passed_distance_check: mult1 = group[index].multiplicity # Find possible wp's to merge into possible = [] for i, wp0 in enumerate(group): mult2 = wp0.multiplicity # Check that a valid orientation exists if orientations is not None: j, k = jk_from_i(i, orientations) if orientations[j][k] == []: continue else: valid_ori = orientations[j][k] # factor = mult2 / mult1 if (mult2 < mult1) and (mult1 % mult2 == 0): possible.append(i) if possible == []: return None, False, valid_ori # Calculate minimum separation for each WP distances = [] for i in possible: wp = group[i] projected_point = project_point(pt, wp[0], lattice=lattice, PBC=PBC) d = distance(pt - projected_point, lattice, PBC=PBC) distances.append(np.min(d)) # Choose wp with shortest translation for generating point tmpindex = np.argmin(distances) index = possible[tmpindex] wp = group[index] pt = project_point(pt, wp[0], lattice=lattice, PBC=PBC) coor = apply_ops(pt, wp) # Distances were not too small; return True else: return pt, wp, valid_ori
def update(self, pos): """ Used to generate coords from self.position """ self.coords = apply_ops(pos, self.wp) self.position = self.coords[0]
def symmetrize(self, splitter, mapping, disp): """ For a given solution, search for the possbile supergroup structure Args: splitter: splitter object to specify the relation between G and H disp: an overall shift from H to G, None or 3 vector d_tol: the tolerance in angstrom Returns: coords_G1: coordinates in G coords_G2: coordinates in G under the subgroup setting coords_H1: coordinates in H elements: list of elements """ cell = np.dot(np.linalg.inv(splitter.R[:3,:3]).T, self.struc.lattice.matrix) atom_sites_H = self.struc.atom_sites coords_G1 = [] # position in G coords_G2 = [] # position in G on the subgroup bais coords_H1 = [] # position in H elements = [] rot = splitter.R[:3,:3] # inverse coordinate transformation tran = splitter.R[:3,3] # needs to check inv_rot = np.linalg.inv(rot) ops_G1 = splitter.G[0] # wp1 stores the wyckoff position object of ['2c', '6h', '12i'] for i, wp1 in enumerate(splitter.wp1_lists): if len(splitter.wp2_lists[i]) == 1: op_G1 = splitter.G1_orbits[i][0][0] ops_H = splitter.H_orbits[i][0] base = atom_sites_H[mapping[i][0]].position.copy() #choose the best coord1_H coord1s_H = apply_ops(base, ops_H) ds = [] for coord1_H in coord1s_H: coord1_G2 = coord1_H + disp coord1_G1, diff = search_G1(splitter.G, rot, tran, coord1_G2, wp1, op_G1) ds.append(diff) ds = np.array(ds) minID = np.argmin(ds) coord1_H = coord1s_H[minID] # symmetrize coord_G1 tmp, _ = search_G1(splitter.G, rot, tran, coord1_H+disp, wp1, op_G1) coords_G1.append(tmp) coord1_G2, _ = search_G2(inv_rot, -tran, tmp, coord1_H+disp, self.cell) #print("G2", wp1.letter, coord1_G2, tmp) coords_G2.append(coord1_G2) coords_H1.append(coord1_H) else: if atom_sites_H[mapping[i][0]].wp.letter == splitter.wp2_lists[i][0].letter: coord1_H = atom_sites_H[mapping[i][0]].position.copy() coord2_H = atom_sites_H[mapping[i][1]].position.copy() else: coord2_H = atom_sites_H[mapping[i][0]].position.copy() coord1_H = atom_sites_H[mapping[i][1]].position.copy() coord1_G2 = coord1_H + disp coord2_G2 = coord2_H + disp op_G11 = splitter.G1_orbits[i][0][0] op_G12 = splitter.G1_orbits[i][1][0] if splitter.group_type == 'k': ops_H1 = splitter.H_orbits[i][0] op_G21 = splitter.G2_orbits[i][0][0] ops_G22 = splitter.G2_orbits[i][1] coord11 = coord1_H + disp coord22 = coord2_H + disp for op_G22 in ops_G22: diff = (op_G22.rotation_matrix - op_G21.rotation_matrix).flatten() if np.sum(diff**2) < 1e-3: trans = op_G22.translation_vector - op_G21.translation_vector break trans -= np.round(trans) coords11 = apply_ops(coord1_G2, ops_H1) coords11 += trans tmp, dist = get_best_match(coords11, coord2_G2, self.cell) #print("G1:", tmp) d = coord2_G2 - tmp d -= np.round(d) coord2_G2 -= d/2 coord1_G2 += d/2 coord1_G1, _ = search_G1(splitter.G, rot, tran, tmp, wp1, op_G11) #print(coord1_G2, coord2_G2, np.dot(rot, tmp).T + tran.T) coords_G1.append(coord1_G1) else: # H->G2->G1 coord1_G1, _ = search_G1(splitter.G, rot, tran, coord1_G2, wp1, op_G11) coord2_G1, _ = search_G1(splitter.G, rot, tran, coord2_G2, wp1, op_G12) #find the best match coords11 = apply_ops(coord1_G1, ops_G1) tmp, dist = get_best_match(coords11, coord2_G1, cell) tmp = sym.search_cloest_wp(splitter.G, wp1, op_G12, tmp) # G1->G2->H d = coord2_G1 - tmp d -= np.round(d) coord2_G1 -= d/2 coord1_G1 += d/2 coords_G1.append(tmp) coord1_G2, dist1 = search_G2(inv_rot, -tran, coord1_G1, coord1_H+disp, self.cell) coord2_G2, dist2 = search_G2(inv_rot, -tran, coord2_G1, coord2_H+disp, self.cell) coords_G2.append(coord1_G2) coords_G2.append(coord2_G2) coords_H1.append(coord1_H) coords_H1.append(coord2_H) elements.extend([splitter.elements[i]]*len(splitter.wp2_lists[i])) return coords_G1, coords_G2, coords_H1, elements
def symmetrize_dist(self, splitter, mapping, disp=None, mask=None, d_tol=1.2): """ For a given solution, search for the possbile supergroup structure Args: splitter: splitter object to specify the relation between G and H mapping: list of sites in H, e.g., ['4a', '8b'] disp: an overall shift from H to G, None or 3 vector mask: if need to freeze the direction d_tol: the tolerance in angstrom Returns: distortion cell translation """ cell = np.dot(np.linalg.inv(splitter.R[:3,:3]).T, self.struc.lattice.matrix) max_disps = [] atom_sites_H = self.struc.atom_sites rot = splitter.R[:3,:3] # inverse coordinate transformation tran = splitter.R[:3,3] # needs to check inv_rot = np.linalg.inv(rot) cell = np.dot(np.linalg.inv(splitter.R[:3,:3]).T, self.struc.lattice.matrix) ops_G1 = splitter.G[0] # if there involves wp transformation between frozen points, disp has to be zero for wp2 in splitter.wp2_lists: frozen = True for wp in wp2: if np.linalg.matrix_rank(wp[0].rotation_matrix) > 0: frozen = False if frozen: disp = np.zeros(3) mask = [0, 1, 2] break if mask is not None and disp is not None: disp[mask] = 0 for i, wp1 in enumerate(splitter.wp1_lists): if len(splitter.wp2_lists[i]) == 1: op_G1 = splitter.G1_orbits[i][0][0] ops_H = splitter.H_orbits[i][0] base = atom_sites_H[mapping[i][0]].position.copy() #choose the best coord1_H coord1s_H = apply_ops(base, ops_H) ds = [] for coord1_H in coord1s_H: if disp is not None: coord1_G2 = coord1_H + disp else: coord1_G2 = coord1_H coord1_G1, diff = search_G1(splitter.G, rot, tran, coord1_G2, wp1, op_G1) ds.append(diff) ds = np.array(ds) minID = np.argmin(ds) coord1_H = coord1s_H[minID] if disp is not None: coord1_G2 = coord1_H + disp else: coord1_G2 = coord1_H tmp, _ = search_G1(splitter.G, rot, tran, coord1_G2, wp1, op_G1) # initial guess on disp if disp is None: coord1_G2, dist1 = search_G2(inv_rot, -tran, tmp, coord1_H, None) diff = coord1_G2 - coord1_H diff -= np.round(diff) disp = diff.copy() mask = [] for m in range(3): if abs(diff[m])<1e-4: mask.append(m) dist = 0 else: coord1_G2, dist = search_G2(inv_rot, -tran, tmp, coord1_H+disp, self.cell) if dist < d_tol: #if dist >0: print(dist, coord1_G2, coord1_H+disp) max_disps.append(dist) else: #import sys; sys.exit() #print("--------", wp1.letter, tmp, coord1_G2, coord1_H+disp, dist) return 10000, None, None elif len(splitter.wp2_lists[i]) == 2: # assume zero shift, needs to check if disp is None: disp = np.zeros(3) mask = [0, 1, 2] # H->G2->G1 if atom_sites_H[mapping[i][0]].wp.letter == splitter.wp2_lists[i][0].letter: coord1_H = atom_sites_H[mapping[i][0]].position.copy() coord2_H = atom_sites_H[mapping[i][1]].position.copy() else: coord2_H = atom_sites_H[mapping[i][0]].position.copy() coord1_H = atom_sites_H[mapping[i][1]].position.copy() #print("\n\n\nH", coord1_H, coord2_H) coord1_G2 = coord1_H + disp coord2_G2 = coord2_H + disp if splitter.group_type == 'k': # For t-type splitting, restore the translation symmetry: # e.g. (0.5, 0.5, 0.5), (0.5, 0, 0), .etc # then find the best_match between coord1 and coord2, ops_H1 = splitter.H_orbits[i][0] op_G21 = splitter.G2_orbits[i][0][0] ops_G22 = splitter.G2_orbits[i][1] for op_G22 in ops_G22: diff = (op_G22.rotation_matrix - op_G21.rotation_matrix).flatten() if np.sum(diff**2) < 1e-3: trans = op_G22.translation_vector - op_G21.translation_vector break trans -= np.round(trans) coords11 = apply_ops(coord1_G2, ops_H1) coords11 += trans tmp, dist = get_best_match(coords11, coord2_G2, self.cell) if dist > np.sqrt(2)*d_tol: #print("disp:", disp) #print("G21:", coords11) #print("G22:", coord2_G2) #print("start: ", coord1_H, coord2_H) #res = tmp - coord2_G2 #res -= np.round(res) #print("kkkk", wp1.letter, dist, tmp, coord2_G2, "diff", res) return 10000, None, mask else: d = coord2_G2 - tmp d -= np.round(d) max_disps.append(np.linalg.norm(np.dot(d/2, self.cell))) else: #print("disp", disp) #print("G2", coord1_G2, coord2_G2) op_G11 = splitter.G1_orbits[i][0][0] op_G12 = splitter.G1_orbits[i][1][0] coord1_G1, _ = search_G1(splitter.G, rot, tran, coord1_G2, wp1, op_G11) coord2_G1, _ = search_G1(splitter.G, rot, tran, coord2_G2, wp1, op_G12) #print("G1", coord1_G1, coord2_G1, op_G12.as_xyz_string()) #print(splitter.G.number, splitter.wp1_lists[i].index, coord2_G1, op_G12.as_xyz_string()) coord2_G1 = sym.search_cloest_wp(splitter.G, wp1, op_G12, coord2_G1) #print("G1(symm1)", coord1_G1, coord2_G1) #import sys; sys.exit() #find the best match coords11 = apply_ops(coord1_G1, ops_G1) tmp, dist = get_best_match(coords11, coord2_G1, cell) tmp = sym.search_cloest_wp(splitter.G, wp1, op_G12, tmp) # G1->G2->H d = coord2_G1 - tmp d -= np.round(d) #print("dist", np.linalg.norm(d), "d", d, tmp, coord2_G1) coord2_G1 -= d/2 coord1_G1 += d/2 #print("G1 (symm2)", coord1_G1, coord2_G1) coord1_G2, dist1 = search_G2(inv_rot, -tran, coord1_G1, coord1_H+disp, self.cell) coord2_G2, dist2 = search_G2(inv_rot, -tran, coord2_G1, coord2_H+disp, self.cell) #print(wp1.letter, dist1, dist2) if max([dist1, dist2]) > np.sqrt(2)*d_tol: #print("1:", coord1_G2, coord1_H, dist1) #print("2:", coord2_G2, coord2_H, dist2) #print("T:", tmp) return 10000, None, mask else: max_disps.append(max([dist1, dist2])) else: raise RuntimeError("donot support merging 3 sites to 1 site") #print(max_disps) return max(max_disps), disp, mask