def load_dict(self, dict0): """ load the structure from a dictionary """ self.group = Group(dict0["group"]) self.lattice = Lattice.from_matrix(dict0["lattice"], ltype=self.group.lattice_type) self.molecular = dict0["molecular"] self.number = self.group.number self.factor = dict0["factor"] self.source = dict0["source"] self.dim = dict0["dim"] self.PBC = dict0["PBC"] self.numIons = dict0["numIons"] self.numMols = dict0["numMols"] self.valid = dict0["valid"] self.formula = dict0["formula"] sites = [] if dict0["molecular"]: for site in dict0["sites"]: sites.append(mol_site.load_dict(site)) self.mol_sites = sites else: for site in dict0["sites"]: sites.append(atom_site.load_dict(site)) self.atom_sites = sites
def test_cluster(): global outstructs global outstrings print( "=== Testing generation of point group clusters. This may take some time. ===" ) from time import time from spglib import get_symmetry_dataset from pyxtal.symmetry import Group from pyxtal.crystal import random_cluster from pymatgen.symmetry.analyzer import SpacegroupAnalyzer slow = [] failed = [] print(" Point group # | Symbol | Time Elapsed") skip = [] # [32,55,56]#[28,29,30,31,32,55,56] for sg in range(1, 57): if sg not in skip: multiplicity = len(Group( sg, dim=0)[0]) # multiplicity of the general position start = time() rand_crystal = random_cluster(sg, ["C"], [multiplicity], 1.0) end = time() timespent = np.around((end - start), decimals=2) t = str(timespent) if len(t) == 3: t += "0" t += " s" if timespent >= 1.0: t += " ~" if timespent >= 3.0: t += "~" if timespent >= 10.0: t += "~" if timespent >= 60.0: t += "~" slow.append(sg) if rand_crystal.valid: if check_struct_group(rand_crystal, sg, dim=0): pass else: t += " xxxxx" outstructs.append(rand_crystal.struct) outstrings.append(str("Cluster_" + str(sg) + ".vasp")) pgsymbol = Group(sg, dim=0).symbol print("\t" + str(sg) + "\t|\t" + pgsymbol + "\t|\t" + t) else: print("~~~~ Error: Could not generate space group " + str(sg) + " after " + t) failed.append(sg) if slow != []: print( "~~~~ The following space groups took more than 60 seconds to generate:" ) for i in slow: print(" " + str(i)) if failed != []: print("~~~~ The following space groups failed to generate:") for i in failed: print(" " + str(i))
def from_pymatgen(self, structure): """ Load the seed structure from Pymatgen/ASE/POSCAR/CIFs """ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as sga try: # needs to do it twice in order to get the conventional cell s = sga(structure) structure = s.get_refined_structure() s = sga(structure) sym_struc = s.get_symmetrized_structure() number = s.get_space_group_number() except: print("Failed to load the Pymatgen structure") self.valid = False if self.valid: d = sym_struc.composition.as_dict() species = [key for key in d.keys()] numIons = [] for ele in species: numIons.append(int(d[ele])) self.numIons = numIons self.species = species self.group = Group(number) atom_sites = [] for i, site in enumerate(sym_struc.equivalent_sites): pos = site[0].frac_coords wp = Wyckoff_position.from_group_and_index( number, sym_struc.wyckoff_symbols[i]) specie = site[0].specie.number atom_sites.append(atom_site(wp, pos, specie)) self.atom_sites = atom_sites self.lattice = Lattice.from_matrix(sym_struc.lattice.matrix, ltype=self.group.lattice_type)
def test_swap_wp(self): g = Group(38) wp = g[4] wp1, trans = wp.swap_axis([1, 0, 2]) g = Group(71) wp = g[5] wp1, trans = wp.swap_axis([0, 2, 1]) wp1, trans = wp.swap_axis([1, 2, 0]) wp1, trans = wp.swap_axis([2, 1, 0])
def test_cluster(): global outstructs global outstrings fprint("=== Testing generation of point group clusters. This may take some time. ===") slow = [] failed = [] fprint(" Point group # | Symbol | Time Elapsed") skip = [56] # [32,55,56]#[28,29,30,31,32,55,56] for sg in range(1, 57): if sg not in skip: multiplicity = len( Group(sg, dim=0)[0] ) # multiplicity of the general position start = time() rand_crystal = pyxtal() rand_crystal.from_random(0, sg, ["C"], [multiplicity], 1.0) end = time() timespent = np.around((end - start), decimals=2) t = str(timespent) if len(t) == 3: t += "0" t += " s" if timespent >= 1.0: t += " ~" if timespent >= 3.0: t += "~" if timespent >= 10.0: t += "~" if timespent >= 60.0: t += "~" slow.append(sg) if rand_crystal.valid: if check_struct_group(rand_crystal, sg, dim=0): pass else: t += " xxxxx" outstructs.append(rand_crystal.to_pymatgen()) outstrings.append(str("Cluster_" + str(sg) + ".vasp")) pgsymbol = Group(sg, dim=0).symbol fprint("\t{}\t|\t{}\t|\t{}".format(sg, pgsymbol, t)) else: fprint( "~~~~ Error: Could not generate space group {} after {}".format(sg, t) ) failed.append(sg) if slow != []: fprint("~~~~ The following space groups took more than 60 seconds to generate:") for i in slow: fprint(" " + str(i)) if failed != []: fprint("~~~~ The following space groups failed to generate:") for i in failed: fprint(" " + str(i))
def from_seed(self, seed, molecule=None, tol=1e-4, relax_h=False, backend='pymatgen'): """ Load the seed structure from Pymatgen/ASE/POSCAR/CIFs Internally they will be handled by Pymatgen """ from ase import Atoms from pymatgen import Structure if self.molecular: from pyxtal.molecule import pyxtal_molecule pmol = pyxtal_molecule(molecule).mol struc = structure_from_ext(seed, pmol, relax_h=relax_h) if struc.match(): self.mol_sites = [struc.make_mol_site()] self.group = Group(struc.wyc.number) self.lattice = struc.lattice self.molecules = [ pyxtal_molecule(struc.molecule, symmetrize=False) ] self.numMols = struc.numMols self.diag = struc.diag self.valid = True # Need to add a check function else: raise ValueError( "Cannot extract the molecular crystal from cif") else: if isinstance(seed, dict): self.from_dict() elif isinstance(seed, Atoms): #ASE atoms from pymatgen.io.ase import AseAtomsAdaptor pmg_struc = AseAtomsAdaptor.get_structure(seed) self._from_pymatgen(pmg_struc, tol) elif isinstance(seed, Structure): #Pymatgen self._from_pymatgen(seed, tol) elif isinstance(seed, str): if backend == 'pymatgen': pmg_struc = Structure.from_file(seed) self._from_pymatgen(pmg_struc, tol) else: self.lattice, self.atom_sites = read_cif(seed) self.group = Group(self.atom_sites[0].wp.number) self.diag = self.atom_sites[0].diag self.valid = True self.factor = 1.0 self.source = 'Seed' self.dim = 3 self.PBC = [1, 1, 1] self._get_formula()
def test_molecular_2D(): global outstructs global outstrings print( "=== Testing generation of molecular 2D crystals. This may take some time. ===" ) from time import time from pyxtal.symmetry import Group from pyxtal.molecular_crystal import molecular_crystal_2D from pymatgen.symmetry.analyzer import SpacegroupAnalyzer slow = [] failed = [] print(" Layer group # | Symbol | Time Elapsed") skip = [] for sg in range(1, 81): if sg not in skip: g = Group(sg, dim=2) multiplicity = len(g[0]) # multiplicity of the general position start = time() rand_crystal = molecular_crystal_2D(sg, ["H2O"], [multiplicity], 4.0) end = time() timespent = np.around((end - start), decimals=2) t = str(timespent) if len(t) == 3: t += "0" t += " s" if timespent >= 1.0: t += " ~" if timespent >= 3.0: t += "~" if timespent >= 10.0: t += "~" if timespent >= 60.0: t += "~" slow.append(sg) if rand_crystal.valid: if check_struct_group(rand_crystal, sg, dim=2): pass else: t += " xxxxx" outstructs.append(rand_crystal.struct) outstrings.append(str("molecular_2D_" + str(sg) + ".vasp")) symbol = g.symbol print("\t" + str(sg) + "\t|\t" + symbol + "\t|\t" + t) else: print("~~~~ Error: Could not generate layer group " + str(sg) + " after " + t) failed.append(sg) if slow != []: print( "~~~~ The following layer groups took more than 60 seconds to generate:" ) for i in slow: print(" " + str(i)) if failed != []: print("~~~~ The following layer groups failed to generate:") for i in failed: print(" " + str(i))
def symmetrize(pmg, tol=1e-3, a_tol=5.0): """ symmetrize the structure from spglib Args: pmg: pymatgen structure tol: tolerance Returns: pymatgen structure with symmetrized lattice """ atoms = (pmg.lattice.matrix, pmg.frac_coords, pmg.atomic_numbers) dataset = get_symmetry_dataset(atoms, tol, angle_tolerance=a_tol) hn = Group(dataset['number'], 3).hall_number if hn != dataset['hall_number']: dataset = get_symmetry_dataset(atoms, tol, angle_tolerance=a_tol, hall_number=hn) cell = dataset['std_lattice'] pos = dataset['std_positions'] numbers = dataset['std_types'] return Structure(cell, numbers, pos)
def _from_pymatgen(self, struc, tol=1e-3, a_tol=5.0): """ Load structure from Pymatgen should not be used directly """ from pyxtal.util import get_symmetrized_pmg #import pymatgen.analysis.structure_matcher as sm self.valid = True try: sym_struc, number = get_symmetrized_pmg(struc, tol, a_tol) #print(sym_struc) #import sys; sys.exit() except TypeError: print("Failed to load the Pymatgen structure") # print(struc) # self.valid = False if self.valid: d = sym_struc.composition.as_dict() species = [key for key in d.keys()] numIons = [] for ele in species: numIons.append(int(d[ele])) self.numIons = numIons self.species = species self.group = Group(number) matrix, ltype = sym_struc.lattice.matrix, self.group.lattice_type self.lattice = Lattice.from_matrix(matrix, ltype=ltype) atom_sites = [] for i, site in enumerate(sym_struc.equivalent_sites): pos = site[0].frac_coords wp = Wyckoff_position.from_group_and_index(number, sym_struc.wyckoff_symbols[i]) specie = site[0].specie.number pos1 = search_matched_position(self.group, wp, pos) if pos1 is not None: atom_sites.append(atom_site(wp, pos1, specie)) else: break if len(atom_sites) != len(sym_struc.equivalent_sites): raise RuntimeError("Cannot extract the right mapping from spglib") else: self.atom_sites = atom_sites
def read_cif(filename): """ read the cif, mainly for pyxtal cif output Be cautious in using it to read other cif files Args: filename: path of the structure file Return: pyxtal structure """ species = [] coords = [] with open(filename, 'r') as f: lines = f.readlines() for i, line in enumerate(lines): if line.startswith('_symmetry_Int_Tables_number'): sg = int(line.split()[-1]) elif line.startswith('_cell_length_a'): a = float(lines[i].split()[-1]) b = float(lines[i+1].split()[-1]) c = float(lines[i+2].split()[-1]) alpha = float(lines[i+3].split()[-1]) beta = float(lines[i+4].split()[-1]) gamma = float(lines[i+5].split()[-1]) elif line.startswith('_symmetry_cell_setting'): lat_type = line.split()[-1] elif line.startswith('_symmetry_space_group_name_H-M '): symbol = line.split()[-1] if eval(symbol) in ["Pn", "P21/n", "C2/n"]: diag = True else: diag = False elif line.find('_atom_site') >= 0: s = i while True: s += 1 if lines[s].find('_atom_site') >= 0: pass elif len(lines[s].split()) <= 3: break else: tmp = lines[s].split() pos = [float(tmp[-4]), float(tmp[-3]), float(tmp[-2])] species.append(tmp[0]) coords.append(pos) break wp0 = Group(sg)[0] lattice = Lattice.from_para(a, b, c, alpha, beta, gamma, lat_type) sites = [] for specie, coord in zip(species, coords): pt, wp, _ = WP_merge(coord, lattice.matrix, wp0, tol=0.1) sites.append(atom_site(wp, pt, specie, diag)) return lattice, sites
def test_molecular_2D(): global outstructs global outstrings fprint( "=== Testing generation of molecular 2D crystals. This may take some time. ===" ) slow = [] failed = [] fprint(" Layer group # | Symbol | Time Elapsed") skip = [] for sg in range(1, 81): if sg not in skip: g = Group(sg, dim=2) multiplicity = len(g[0]) # multiplicity of the general position start = time() rand_crystal = pyxtal(molecular=True) rand_crystal.from_random(2, sg, ["H2O"], [multiplicity], 4.0) end = time() timespent = np.around((end - start), decimals=2) t = str(timespent) if len(t) == 3: t += "0" t += " s" if timespent >= 1.0: t += " ~" if timespent >= 3.0: t += "~" if timespent >= 10.0: t += "~" if timespent >= 60.0: t += "~" slow.append(sg) if rand_crystal.valid: if check_struct_group(rand_crystal, sg, dim=2): pass else: t += " xxxxx" outstructs.append(rand_crystal.to_pymatgen()) outstrings.append(str("molecular_2D_" + str(sg) + ".vasp")) symbol = g.symbol fprint("\t{}\t|\t{}\t|\t{}".format(sg, symbol, t)) else: fprint( "~~~~ Error: Could not generate layer group {} after {}".format(sg, t) ) failed.append(sg) if slow != []: fprint("~~~~ The following layer groups took more than 60 seconds to generate:") for i in slow: fprint(" " + str(i)) if failed != []: fprint("~~~~ The following layer groups failed to generate:") for i in failed: fprint(" " + str(i))
def make_gen_wyckoff_site(self): if self.wp.index > 0: wp, ax, pos = self._find_gen_wyckoff_in_subgroup() ori = self.orientation.rotate_by_matrix(np.eye(3)[ax]) lat = self.lattice.swap_axis(ids=ax) lat.ltype = Group(wp.number).lattice_type return mol_site(self.molecule, pos, ori, wp, lat, self.diag) else: print("This is already a general position") return self
def _from_pymatgen(self, struc, tol=1e-3): """ Load structure from Pymatgen should not be used directly """ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as sga from pyxtal.util import symmetrize #import pymatgen.analysis.structure_matcher as sm self.valid = True try: # needs to do it twice in order to get the conventional cell pmg = symmetrize(struc, tol) s = sga(pmg, symprec=tol) sym_struc = s.get_symmetrized_structure() number = s.get_space_group_number() #print(sym_struc) except: print("Failed to load the Pymatgen structure") self.valid = False if self.valid: d = sym_struc.composition.as_dict() species = [key for key in d.keys()] numIons = [] for ele in species: numIons.append(int(d[ele])) self.numIons = numIons self.species = species self.group = Group(number) atom_sites = [] for i, site in enumerate(sym_struc.equivalent_sites): pos = site[0].frac_coords wp = Wyckoff_position.from_group_and_index( number, sym_struc.wyckoff_symbols[i]) specie = site[0].specie.number atom_sites.append(atom_site(wp, pos, specie, search=True)) self.atom_sites = atom_sites matrix, ltype = sym_struc.lattice.matrix, self.group.lattice_type self.lattice = Lattice.from_matrix(matrix, ltype=ltype)
def test_molecular_nodiag(self): sgs, diag = [5, 7, 8, 12, 13, 14], False for i in range(40): sg = choice(sgs) num = len(Group(sg)[0]) c1 = pyxtal(molecular=True) c1.from_random(3, sg, ["aspirin"], [num], diag=diag) pmg1 = c1.to_pymatgen() c2 = c1.copy() c2.optimize_lattice(1) pmg2 = c2.to_pymatgen() self.assertTrue(sm.StructureMatcher().fit(pmg1, pmg2))
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 __init__(self, struc, ref_mols, tol=0.2, relax_h=False): """ extract the mol_site information from the give cif file and reference molecule Args: struc: cif/poscar file or a Pymatgen Structure object ref_mols: a list of reference molecule (xyz file or Pyxtal molecule) tol: scale factor for covalent bond distance relax_h: whether or not relax the position for hydrogen atoms in structure """ for ref_mol in ref_mols: if isinstance(ref_mol, str): ref_mol = pyxtal_molecule(ref_mol) elif isinstance(ref_mol, pyxtal_molecule): ref_mol = ref_mol else: print(type(ref_mol)) raise NameError("reference molecule cannot be defined") if isinstance(struc, str): pmg_struc = Structure.from_file(struc) elif isinstance(struc, Structure): pmg_struc = struc else: print(type(struc)) raise NameError("input structure cannot be intepretted") self.ref_mols = ref_mols self.tol = tol self.diag = False self.relax_h = relax_h sym_struc, number = get_symmetrized_pmg(pmg_struc) group = Group(number) self.group = group self.wyc = group[0] self.perm = [0,1,2] molecules = search_molecules_in_crystal(sym_struc, self.tol) if self.relax_h: molecules = self.addh(molecules) self.pmg_struc = sym_struc self.lattice = Lattice.from_matrix(sym_struc.lattice.matrix, ltype=group.lattice_type) self.resort(molecules) self.numMols = [len(self.wyc)]
def __init__( self, group=None, species=None, numIons=None, factor=1.1, lattice=None, sites=None, conventional=True, tm=Tol_matrix(prototype="atomic"), ): self.dim = 3 #periodic dimensions of the crystal self.PBC = [1, 1, 1] #The periodic boundary axes of the crystal if type(group) != Group: group = Group(group, self.dim) self.init_common(species, numIons, factor, group, lattice, sites, conventional, tm)
def __init__( self, group, species, numIons, factor=1.1, thickness=None, lattice=None, sites=None, tm=Tol_matrix(prototype="atomic"), ): self.dim = 2 self.PBC = [1, 1, 0] if type(group) != Group: group = Group(group, self.dim) number = group.number # The layer group number of the crystal self.thickness = thickness # in Angstroms, in the 3rd dimenion of unit cell self.init_common(species, numIons, factor, number, lattice, sites, tm)
def __init__( self, group, molecules, numMols, volume_factor=1.1, select_high=True, allow_inversion=True, orientations=None, lattice=None, tm=Tol_matrix(prototype="molecular"), sites=None, seed=None, diag=False, relax_h=False, ): self.dim = 3 # The number of periodic dimensions (1,2,3) self.PBC = [1, 1, 1] self.diag = diag if type(group) != Group: group = Group(group, self.dim) self.sg = group.number self.selec_high = select_high self.seed = seed self.relax_h = relax_h self.init_common( molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, tm, sites, )
def get_symmetrized_pmg(pmg, tol=1e-3, a_tol=5.0): """ Symmetrized Pymatgen structure A slight modification to ensure that the structure adopts the standard setting used in interational crystallography table Args: pmg: input pymatgen structure tol: symmetry tolerance """ pmg = symmetrize(pmg, tol, a_tol=a_tol) s = sga(pmg, symprec=tol, angle_tolerance=a_tol) hn = Group(s.get_space_group_number()).hall_number # make sure that the coordinates are in standard setting if hn != s._space_group_data["hall_number"]: s._space_group_data = get_symmetry_dataset(s._cell, tol, angle_tolerance=a_tol, hall_number=hn) return s.get_symmetrized_structure(), s.get_space_group_number()
def __init__( self, group, molecules, numMols, volume_factor=1.1, select_high=True, allow_inversion=True, orientations=None, thickness=None, lattice=None, tm=Tol_matrix(prototype="molecular"), seed=None, sites=None, ): self.dim = 2 self.numattempts = 0 self.seed = None if type(group) != Group: group = Group(group, self.dim) number = group.number # The layer group number of the crystal.""" self.diag = False self.thickness = thickness # the thickness in Angstroms self.PBC = [1, 1, 0] self.init_common( molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, tm, sites, )
def __init__( self, group=None, species=None, numIons=None, factor=1.1, lattice=None, sites=None, tm=Tol_matrix(prototype="atomic"), seed=None, ): self.dim = 3 #periodic dimensions of the crystal self.PBC = [1, 1, 1] #The periodic boundary axes of the crystal if seed is None: if type(group) != Group: group = Group(group, self.dim) self.sg = group.number #The international spacegroup number self.seed = None self.init_common(species, numIons, factor, group, lattice, sites, tm) else: self.seed = seed self.from_seed()
from pyxtal.symmetry import Group length1 = 0 length2 = 0 length3 = 0 length_greater = 0 for g in range(1, 231): wyc = Group(g).get_max_t_subgroup() for re, h in zip(wyc['relations'], wyc['subgroup']): for pos in re: length = len(pos) if length == 1: length1 += 1 elif length == 2: length2 += 1 elif length == 3: length3 += 1 #print(g, h, pos) else: length_greater += 1 print(g, h, pos)
def init_common( self, molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, tm, sites, ): # init functionality which is shared by 3D, 2D, and 1D crystals self.valid = False self.numattempts = 0 # number of attempts to generate the crystal. if type(group) == Group: self.group = group """A pyxtal.symmetry.Group object storing information about the space/layer /Rod/point group, and its Wyckoff positions.""" else: self.group = Group(group, dim=self.dim) self.number = self.group.number """ The international group number of the crystal: 1-230 for 3D space groups 1-80 for 2D layer groups 1-75 for 1D Rod groups 1-32 for crystallographic point groups None otherwise """ self.Msgs() self.factor = volume_factor # volume factor for the unit cell. numMols = np.array(numMols) # must convert it to np.array self.numMols0 = numMols # in the PRIMITIVE cell self.numMols = self.numMols0 * cellsize( self.group) # in the CONVENTIONAL cell # boolean numbers self.allow_inversion = allow_inversion self.select_high = select_high # Set the tolerance matrix # The Tol_matrix object for checking inter-atomic distances within the structure. if type(tm) == Tol_matrix: self.tol_matrix = tm else: try: self.tol_matrix = Tol_matrix(prototype=tm) # TODO remove bare except except: msg = "Error: tm must either be a Tol_matrix object +\n" msg += "or a prototype string for initializing one." printx(msg, priority=1) return self.molecules = [] # A pyxtal_molecule objects, for mol in molecules: self.molecules.append(pyxtal_molecule(mol, self.tol_matrix)) self.sites = {} for i, mol in enumerate(self.molecules): if sites is not None and sites[i] is not None: self.check_consistency(sites[i], self.numMols[i]) self.sites[i] = sites[i] else: self.sites[i] = None # if seeds, directly parse the structure from cif # At the moment, we only support one specie if self.seed is not None: seed = structure_from_ext(self.seed, self.molecules[0].mol, relax_h=self.relax_h) if seed.match(): self.mol_sites = [seed.make_mol_site()] self.group = Group(seed.wyc.number) self.lattice = seed.lattice self.molecules = [pyxtal_molecule(seed.molecule)] self.diag = seed.diag self.valid = True # Need to add a check function else: raise ValueError("Cannot extract the structure from cif") # The valid orientations for each molecule and Wyckoff position. # May be copied when generating a new molecular_crystal to save a # small amount of time if orientations is None: self.get_orientations() else: self.valid_orientations = orientations if self.seed is None: if lattice is not None: # Use the provided lattice self.lattice = lattice self.volume = lattice.volume # Make sure the custom lattice PBC axes are correct. if lattice.PBC != self.PBC: self.lattice.PBC = self.PBC printx("\n Warning: converting custom lattice PBC to " + str(self.PBC)) else: # Determine the unique axis if self.dim == 2: if self.number in range(3, 8): unique_axis = "c" else: unique_axis = "a" elif self.dim == 1: if self.number in range(3, 8): unique_axis = "a" else: unique_axis = "c" else: unique_axis = "c" # Generate a Lattice instance self.volume = self.estimate_volume() # The Lattice object used to generate lattice matrices if self.dim == 3 or self.dim == 0: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, ) elif self.dim == 2: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, thickness=self.thickness, ) elif self.dim == 1: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, area=self.area, ) self.generate_crystal()
from pyxtal import pyxtal from pyxtal.symmetry import Group import pymatgen.analysis.structure_matcher as sm from pymatgen.symmetry.analyzer import SpacegroupAnalyzer import numpy as np for G in range(143, 195): g = Group(G) letter = str(g[0].multiplicity) + g[0].letter C1 = pyxtal() C1.from_random(3, G, ['C'], [g[0].multiplicity], sites=[[letter]]) #print(C1) pmg_s1 = C1.to_pymatgen() sga1 = SpacegroupAnalyzer(pmg_s1).get_space_group_symbol() # each subgroup #C2s = C1.subgroup(eps=0, group_type='t') try: C2s = C1.subgroup(eps=0, group_type='k', max_cell=4) for C2 in C2s: #print(C2) pmg_s2 = C2.to_pymatgen() try: sga2 = SpacegroupAnalyzer( pmg_s2, symprec=1e-4).get_space_group_symbol() except: #print("unable to find the space group") sga2 = None print(G, C2.group.number, g.symbol, C2.group.symbol, sga1, sga2) if not sm.StructureMatcher().fit(pmg_s1, pmg_s2): print('WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW')
def init_common(self, species, numIons, factor, group, lattice, sites, conventional, tm): """ Common init functionality for 0D-3D cases of random_crystal. """ self.source = 'Random' self.valid = False # Check that numIons are integers greater than 0 for num in numIons: if int(num) != num or num < 1: printx("Error: composition must be positive integers.", priority=1) return False if type(group) == Group: self.group = group else: self.group = Group(group, dim=self.dim) self.number = self.group.number """ The international group number of the crystal: 1-230 for 3D space groups 1-80 for 2D layer groups 1-75 for 1D Rod groups 1-32 for crystallographic point groups None otherwise """ # The number of attempts to generate the crystal # number of atoms # volume factor for the unit cell. # The number of atom in the PRIMITIVE cell # The number of each type of atom in the CONVENTIONAL cell. # A list of atomic symbols for the types of atoms # A list of warning messages self.numattempts = 0 numIons = np.array(numIons) self.factor = factor if not conventional: mul = cellsize(self.group) else: mul = 1 self.numIons = numIons * mul formula = "" for i, s in zip(self.numIons, species): formula += "{:s}{:d}".format(s, int(i)) self.formula = formula self.species = species # Use the provided lattice if lattice is not None: self.lattice = lattice self.volume = lattice.volume # Make sure the custom lattice PBC axes are correct. if lattice.PBC != self.PBC: self.lattice.PBC = self.PBC printx("\n Warning: converting custom lattice PBC to " + str(self.PBC)) # Generate a Lattice instance based on a given volume estimation elif lattice is None: # Determine the unique axis if self.dim == 2: if self.number in range(3, 8): unique_axis = "c" else: unique_axis = "a" elif self.dim == 1: if self.number in range(3, 8): unique_axis = "a" else: unique_axis = "c" else: unique_axis = "c" self.volume = self.estimate_volume() if self.dim == 3 or self.dim == 0: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, ) elif self.dim == 2: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, # NOTE self.thickness is part of 2D class thickness=self.thickness, ) elif self.dim == 1: self.lattice = Lattice( self.group.lattice_type, self.volume, PBC=self.PBC, unique_axis=unique_axis, # NOTE self.area is part of 1D class area=self.area, ) # Set the tolerance matrix for checking inter-atomic distances if type(tm) == Tol_matrix: self.tol_matrix = tm else: try: self.tol_matrix = Tol_matrix(prototype=tm) # TODO Remove bare except except: printx( ("Error: tm must either be a Tol_matrix object or " "a prototype string for initializing one."), priority=1, ) self.valid = False return self.sites = {} for i, specie in enumerate(self.species): if sites is not None and sites[i] is not None: self.check_consistency(sites[i], self.numIons[i]) self.sites[specie] = sites[i] else: self.sites[specie] = None # QZ: needs to check if it is compatible self.generate_crystal()
def check_struct_group(crystal, group, dim=3, tol=1e-2): # Supress pymatgen/numpy complex casting warnings from pyxtal.crystal import random_crystal from pyxtal.molecular_crystal import molecular_crystal from copy import deepcopy import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") """Given a pymatgen structure, group number, and dimension, return whether or not the structure matches the group number.""" if isinstance(crystal, (random_crystal, molecular_crystal)): lattice = crystal.struct.lattice.matrix if dim != 0: old_coords = deepcopy(crystal.struct.frac_coords) old_species = deepcopy(crystal.struct.atomic_numbers) elif dim == 0: old_coords = deepcopy(crystal.cart_coords) old_species = deepcopy(crystal.species) else: lattice = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) old_coords = np.array(crystal) old_species = ["C"] * len(old_coords) from pyxtal.symmetry import distance from pyxtal.symmetry import filtered_coords from copy import deepcopy PBC = [1, 1, 1] # Obtain the generators for the group if dim == 3: from pyxtal.symmetry import get_wyckoffs generators = get_wyckoffs(group)[0] elif dim == 2: from pyxtal.symmetry import get_layer generators = get_layer(group)[0] PBC = [1, 1, 0] elif dim == 1: from pyxtal.symmetry import get_rod generators = get_rod(group)[0] PBC = [0, 0, 1] elif dim == 0: from pyxtal.symmetry import Group generators = Group(group, dim=0)[0] PBC = [0, 0, 0] # TODO: Add check for lattice symmetry # Apply SymmOps to generate new points # old_coords = filtered_coords(struct.frac_coords,PBC=PBC) new_coords = [] new_species = [] for i, point in enumerate(old_coords): for j, op in enumerate(generators): if j != 0: new_coords.append(op.operate(point)) new_species.append(old_species[i]) # new_coords = filtered_coords(new_coords,PBC=PBC) # Check that all points in new list are still in old failed = False i_list = list(range(len(new_coords))) for i, point1 in enumerate(new_coords): found = False for j, point2 in enumerate(old_coords): if new_species[i] == old_species[j]: difference = filtered_coords(point2 - point1, PBC=PBC) if distance(difference, lattice, PBC=PBC) <= tol: found = True break if found is False: failed = True break if failed is False: return True else: return False
def test_modules(): print("====== Testing functionality for pyXtal version 0.1dev ======") global failed_package failed_package = False # Record if errors occur at any level reset() print("Importing sys...") try: import sys print("Success!") except Exception as e: fail(e) sys.exit(0) print("Importing numpy...") try: import numpy as np print("Success!") except Exception as e: fail(e) sys.exit(0) I = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) print("Importing pymatgen...") try: import pymatgen print("Success!") except Exception as e: fail(e) sys.exit(0) try: from pymatgen.core.operations import SymmOp except Exception as e: fail(e) sys.exit(0) print("Importing pandas...") try: import pandas print("Success!") except Exception as e: fail(e) sys.exit(0) print("Importing spglib...") try: import spglib print("Success!") except Exception as e: fail(e) sys.exit(0) print("Importing openbabel...") try: import ase print("Success!") except: print( "Error: could not import openbabel. Try reinstalling the package.") print("Importing pyxtal...") try: import pyxtal print("Success!") except Exception as e: fail(e) sys.exit(0) print("=== Testing modules ===") # =====database.element===== print("pyxtal.database.element") reset() try: import pyxtal.database.element except Exception as e: fail(e) print(" class Element") try: from pyxtal.database.element import Element except Exception as e: fail(e) if passed(): for i in range(1, 95): if passed(): try: ele = Element(i) except: fail("Could not access Element # " + str(i)) try: y = ele.sf y = ele.z y = ele.short_name y = ele.long_name y = ele.valence y = ele.valence_electrons y = ele.covalent_radius y = ele.vdw_radius y = ele.get_all(0) except: fail("Could not access attribute for element # " + str(i)) try: ele.all_z() ele.all_short_names() ele.all_long_names() ele.all_valences() ele.all_valence_electrons() ele.all_covalent_radii() ele.all_vdw_radii() except: fail("Could not access class methods") check() # =====database.hall===== print("pyxtal.database.hall") reset() try: import pyxtal.database.hall except Exception as e: fail(e) print(" hall_from_hm") try: from pyxtal.database.hall import hall_from_hm except Exception as e: fail(e) if passed(): for i in range(1, 230): if passed(): try: hall_from_hm(i) except: fail("Could not access hm # " + str(i)) check() # =====database.collection===== print("pyxtal.database.collection") reset() try: import pyxtal.database.collection except Exception as e: fail(e) print(" Collection") try: from pyxtal.database.collection import Collection except Exception as e: fail(e) if passed(): for i in range(1, 230): if passed(): try: molecule_collection = Collection("molecules") except: fail("Could not access hm # " + str(i)) check() # =====operations===== print("pyxtal.operations") reset() try: import pyxtal.operations except Exception as e: fail(e) print(" random_vector") try: from pyxtal.operations import random_vector except Exception as e: fail(e) if passed(): try: for i in range(10): random_vector() except Exception as e: fail(e) check() print(" angle") try: from pyxtal.operations import angle except Exception as e: fail(e) if passed(): try: for i in range(10): v1 = random_vector() v2 = random_vector() angle(v1, v2) except Exception as e: fail(e) check() print(" random_shear_matrix") try: from pyxtal.operations import random_shear_matrix except Exception as e: fail(e) if passed(): try: for i in range(10): random_shear_matrix() except Exception as e: fail(e) check() print(" is_orthogonal") try: from pyxtal.operations import is_orthogonal except Exception as e: fail(e) if passed(): try: a = is_orthogonal([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) b = is_orthogonal([[0, 0, 1], [1, 0, 0], [1, 0, 0]]) if a is True and b is False: pass else: fail() except Exception as e: fail(e) check() print(" aa2matrix") try: from pyxtal.operations import aa2matrix except Exception as e: fail(e) if passed(): try: for i in range(10): aa2matrix(1, 1, random=True) except Exception as e: fail(e) check() print(" matrix2aa") try: from pyxtal.operations import matrix2aa except Exception as e: fail(e) if passed(): try: for i in range(10): m = aa2matrix(1, 1, random=True) aa = matrix2aa(m) except Exception as e: fail(e) check() print(" rotate_vector") try: from pyxtal.operations import rotate_vector except Exception as e: fail(e) if passed(): try: for i in range(10): v1 = random_vector() v2 = random_vector() rotate_vector(v1, v2) except Exception as e: fail(e) check() print(" are_equal") try: from pyxtal.operations import are_equal except Exception as e: fail(e) if passed(): try: op1 = SymmOp.from_xyz_string("x,y,z") op2 = SymmOp.from_xyz_string("x,y,z+1") a = are_equal(op1, op2, PBC=[0, 0, 1]) b = are_equal(op1, op2, PBC=[1, 0, 0]) if a is True and b is False: pass else: fail() except Exception as e: fail(e) check() print(" class OperationAnalyzer") try: from pyxtal.operations import OperationAnalyzer except Exception as e: fail(e) if passed(): try: for i in range(10): m = aa2matrix(1, 1, random=True) t = random_vector() op1 = SymmOp.from_rotation_and_translation(m, t) OperationAnalyzer(op1) except Exception as e: fail(e) check() print(" class Orientation") try: from pyxtal.operations import Orientation except Exception as e: fail(e) if passed(): try: for i in range(10): v1 = random_vector() c1 = random_vector() o = Orientation.from_constraint(v1, c1) except Exception as e: fail(e) check() # =====symmetry===== print("pyxtal.symmetry") reset() try: import pyxtal.symmetry except Exception as e: fail(e) print(" get_wyckoffs (may take a moment)") try: from pyxtal.symmetry import get_wyckoffs except Exception as e: fail(e) if passed(): try: for i in [1, 2, 229, 230]: get_wyckoffs(i) get_wyckoffs(i, organized=True) except: fail(" Could not access Wyckoff positions for space group # " + str(i)) check() print(" get_wyckoff_symmetry (may take a moment)") try: from pyxtal.symmetry import get_wyckoff_symmetry except Exception as e: fail(e) if passed(): try: for i in [1, 2, 229, 230]: get_wyckoff_symmetry(i) get_wyckoff_symmetry(i, molecular=True) except: fail("Could not access Wyckoff symmetry for space group # " + str(i)) check() print(" get_wyckoffs_generators (may take a moment)") try: from pyxtal.symmetry import get_wyckoff_generators except Exception as e: fail(e) if passed(): try: for i in [1, 2, 229, 230]: get_wyckoff_generators(i) except: fail("Could not access Wyckoff generators for space group # " + str(i)) check() print(" letter_from_index") try: from pyxtal.symmetry import letter_from_index except Exception as e: fail(e) if passed(): try: if letter_from_index(0, get_wyckoffs(47)) == "A": pass else: fail() except Exception as e: fail(e) check() print(" index_from_letter") try: from pyxtal.symmetry import index_from_letter except Exception as e: fail(e) if passed(): try: if index_from_letter("A", get_wyckoffs(47)) == 0: pass else: fail() except Exception as e: fail(e) check() print(" jk_from_i") try: from pyxtal.symmetry import jk_from_i except Exception as e: fail(e) if passed(): try: w = get_wyckoffs(2, organized=True) j, k = jk_from_i(1, w) if j == 1 and k == 0: pass else: print(j, k) fail() except Exception as e: fail(e) check() print(" i_from_jk") try: from pyxtal.symmetry import i_from_jk except Exception as e: fail(e) if passed(): try: w = get_wyckoffs(2, organized=True) j, k = jk_from_i(1, w) i = i_from_jk(j, k, w) if i == 1: pass else: print(j, k) fail() except Exception as e: fail(e) check() print(" ss_string_from_ops") try: from pyxtal.symmetry import ss_string_from_ops except Exception as e: fail(e) if passed(): try: strings = ["1", "4 . .", "2 3 ."] for i, sg in enumerate([1, 75, 195]): ops = get_wyckoffs(sg)[0] ss_string_from_ops(ops, sg, dim=3) except Exception as e: fail(e) check() print(" Wyckoff_position") try: from pyxtal.symmetry import Wyckoff_position except Exception as e: fail(e) if passed(): try: wp = Wyckoff_position.from_group_and_index(20, 1) except Exception as e: fail(e) check() print(" Group") try: from pyxtal.symmetry import Group except Exception as e: fail(e) if passed(): try: g3 = Group(230) g2 = Group(80, dim=2) g1 = Group(75, dim=1) except Exception as e: fail(e) check() # =====crystal===== print("pyxtal.crystal") reset() try: import pyxtal.crystal except Exception as e: fail(e) print(" random_crystal") try: from pyxtal.crystal import random_crystal except Exception as e: fail(e) if passed(): try: c = random_crystal(1, ["H"], [1], 10.0) if c.valid is True: pass else: fail() except Exception as e: fail(e) check() print(" random_crystal_2D") try: from pyxtal.crystal import random_crystal_2D except Exception as e: fail(e) if passed(): try: c = random_crystal_2D(1, ["H"], [1], 10.0) if c.valid is True: pass else: fail() except Exception as e: fail(e) check() # =====molecule===== print("pyxtal.molecule") reset() try: import pyxtal.molecule except Exception as e: fail(e) check() print(" Collections") try: from pyxtal.molecule import mol_from_collection except Exception as e: fail(e) if passed(): try: h2o = mol_from_collection("H2O") ch4 = mol_from_collection("CH4") except Exception as e: fail(e) print(" get_inertia_tensor") try: from pyxtal.molecule import get_inertia_tensor except Exception as e: fail(e) if passed(): try: get_inertia_tensor(h2o) get_inertia_tensor(ch4) except Exception as e: fail(e) check() print(" get_moment_of_inertia") try: from pyxtal.molecule import get_moment_of_inertia except Exception as e: fail(e) if passed(): try: v = random_vector() get_moment_of_inertia(h2o, v) get_moment_of_inertia(ch4, v) except Exception as e: fail(e) check() print(" reoriented_molecule") try: from pyxtal.molecule import reoriented_molecule except Exception as e: fail(e) if passed(): try: reoriented_molecule(h2o) reoriented_molecule(ch4) except Exception as e: fail(e) check() print(" orientation_in_wyckoff_position") try: from pyxtal.molecule import orientation_in_wyckoff_position except Exception as e: fail(e) if passed(): try: w = get_wyckoffs(20) ws = get_wyckoff_symmetry(20, molecular=True) wp = Wyckoff_position.from_group_and_index(20, 1) orientation_in_wyckoff_position(h2o, wp) orientation_in_wyckoff_position(ch4, wp) except Exception as e: fail(e) check() # =====molecular_crystal===== print("pyxtal.molecular_crystal") reset() try: import pyxtal.crystal except Exception as e: fail(e) print(" molecular_crystal") try: from pyxtal.molecular_crystal import molecular_crystal except Exception as e: fail(e) if passed(): try: c = molecular_crystal(1, ["H2O"], [1], 10.0) if c.valid is True: pass else: fail() except Exception as e: fail(e) check() print(" molecular_crystal_2D") try: from pyxtal.molecular_crystal import molecular_crystal_2D except Exception as e: fail(e) if passed(): try: c = molecular_crystal_2D(1, ["H2O"], [1], 10.0) if c.valid is True: pass else: fail() except Exception as e: fail(e) check() end(condition=2)
# -------------------------------- Options ------------------------- parser = ArgumentParser() parser.add_argument( "-s", "--symmetry", dest="sg", type=str, help="desired symmetry, number or string, e.g., 36, Pbca, Ih. if None, show all list of available groups", ) parser.add_argument( "-d", "--dimension", dest="dimension", default=3, type=int, help="desired dimension: (3, 2, 1, 0): default 3", ) print_logo() options = parser.parse_args() dimension = options.dimension if options.sg is not None: sg = options.sg if sg.isnumeric(): sg = int(sg) Group(sg, dimension).print_all() else: list_groups(dimension)
class pyxtal: """ Class for handling atomic crystals based on symmetry constraints. Examples -------- >>> from pyxtal import pyxtal >>> struc = pyxtal() >>> struc.from_random(3, 227, ['C'], [8]) >>> struc.get_site_labels() {'C': ['8a']} >>> struc ------Crystal from random------ Dimension: 3 Composition: C8 Group: Fd-3m (227) cubic lattice: 4.3529 4.3529 4.3529 90.0000 90.0000 90.0000 Wyckoff sites: C @ [0.1250 0.1250 0.1250], WP: 8a, Site symmetry: -4 3 m The structure object can be easily manipulated via `apply_perturbtation` or `subgroup` function >>> struc2 = struc.subgroup(H=141, once=True) >>> struc2 ------Crystal from Wyckoff Split------ Dimension: 3 Composition: C8 Group: I41/amd (141) tetragonal lattice: 3.3535 3.3535 4.6461 90.0000 90.0000 90.0000 Wyckoff sites: C @ [0.0000 0.2500 0.3750], WP: 4b, Site symmetry: -4 m 2 Alternatively, one can also easily compute its XRD via the `pyxtal.XRD` class >>> xrd = struc.get_XRD() >>> xrd 2theta d_hkl hkl Intensity Multi 32.706 2.738 [ 1 1 1] 100.00 8 54.745 1.677 [ 2 2 0] 40.95 12 65.249 1.430 [ 3 1 1] 20.65 24 81.116 1.186 [ 4 0 0] 5.15 6 90.236 1.088 [ 3 3 1] 8.24 24 105.566 0.968 [ 4 2 2] 14.44 24 115.271 0.913 [ 5 1 1] 10.03 24 133.720 0.838 [ 4 4 0] 9.80 12 148.177 0.802 [ 5 3 1] 28.27 48 Finally, the structure can be saved to different formats >>> struc.to_file('my.cif') >>> struc.to_file('my_poscar', fmt='poscar') or to Pymatgen/ASE structure object >>> pmg_struc = struc.to_pymatgen() >>> ase_struc = struc.to_ase() """ def __init__(self, molecular=False): self.valid = False self.molecular = molecular self.diag = False self.numIons = None self.numMols = None def __str__(self): if self.valid: s = "------Crystal from {:s}------".format(self.source) s += "\nDimension: {}".format(self.dim) s += "\nComposition: {}".format(self.formula) if self.group.number in [7, 14, 15] and self.diag: symbol = self.group.alias else: symbol = self.group.symbol s += "\nGroup: {} ({})".format(symbol, self.group.number) s += "\n{}".format(self.lattice) s += "\nWyckoff sites:" if self.molecular: for wyc in self.mol_sites: s += "\n\t{}".format(wyc) else: for wyc in self.atom_sites: s += "\n\t{}".format(wyc) else: s = "\nStructure not available." return s def __repr__(self): return str(self) def get_dof(self): """ get the number of dof for the given structures: """ if self.molecular: sites = self.mol_sites else: sites = self.atom_sites dof = 0 for site in sites: dof += site.dof return self.lattice.dof + dof def get_site_labels(self): """ quick function to get the site_labels as a dictionary """ if self.molecular: sites = self.mol_sites names = [site.molecule.name for site in sites] else: sites = self.atom_sites names = [site.specie for site in sites] dicts = {} for name, site in zip(names, sites): label = str(site.wp.multiplicity) + site.wp.letter if name not in dicts.keys(): dicts[name] = [label] else: dicts[name].append(label) return dicts def from_random( self, dim=3, group=None, species=None, numIons=None, factor=1.1, thickness=None, area=None, lattice=None, sites=None, conventional=True, diag=False, t_factor=1.0, max_count=10, force_pass=False, ): if self.molecular: prototype = "molecular" else: prototype = "atomic" tm = Tol_matrix(prototype=prototype, factor=t_factor) count = 0 quit = False while True: count += 1 if self.molecular: if dim == 3: struc = molecular_crystal(group, species, numIons, factor, lattice=lattice, sites=sites, conventional=conventional, diag=diag, tm=tm) elif dim == 2: struc = molecular_crystal_2D(group, species, numIons, factor, thickness=thickness, sites=sites, conventional=conventional, tm=tm) elif dim == 1: struc = molecular_crystal_1D(group, species, numIons, factor, area=area, sites=sites, conventional=conventional, tm=tm) else: if dim == 3: struc = random_crystal(group, species, numIons, factor, lattice, sites, conventional, tm) elif dim == 2: struc = random_crystal_2D(group, species, numIons, factor, thickness, lattice, sites, conventional, tm) elif dim == 1: struc = random_crystal_1D(group, species, numIons, factor, area, lattice, sites, conventional, tm) else: struc = random_cluster(group, species, numIons, factor, lattice, sites, tm) if force_pass: quit = True break elif struc.valid: quit = True break if count >= max_count: raise RuntimeError( "It takes long time to generate the structure, check inputs" ) if quit: self.valid = struc.valid self.dim = dim try: self.lattice = struc.lattice if self.molecular: self.numMols = struc.numMols self.molecules = struc.molecules self.mol_sites = struc.mol_sites self.diag = struc.diag else: self.numIons = struc.numIons self.species = struc.species self.atom_sites = struc.atom_sites self.group = struc.group self.PBC = struc.PBC self.source = 'random' self.factor = struc.factor self.number = struc.number self._get_formula() except: pass def from_seed(self, seed, molecule=None, relax_h=False): """ Load the seed structure from Pymatgen/ASE/POSCAR/CIFs Internally they will be handled by Pymatgen """ from ase import Atoms from pymatgen import Structure if self.molecular: from pyxtal.molecule import pyxtal_molecule pmol = pyxtal_molecule(molecule).mol struc = structure_from_ext(seed, pmol, relax_h=relax_h) if struc.match(): self.mol_sites = [struc.make_mol_site()] self.group = Group(struc.wyc.number) self.lattice = struc.lattice self.molecules = [ pyxtal_molecule(struc.molecule, symmetrize=False) ] self.numMols = struc.numMols self.diag = struc.diag self.valid = True # Need to add a check function else: raise ValueError( "Cannot extract the molecular crystal from cif") else: if isinstance(seed, dict): self.from_dict() elif isinstance(seed, Atoms): #ASE atoms from pymatgen.io.ase import AseAtomsAdaptor pmg_struc = AseAtomsAdaptor.get_structure(seed) self._from_pymatgen(pmg_struc) elif isinstance(seed, Structure): #Pymatgen self._from_pymatgen(seed) elif isinstance(seed, str): pmg_struc = Structure.from_file(seed) self._from_pymatgen(pmg_struc) self.factor = 1.0 self.number = self.group.number self.source = 'Seed' self.dim = 3 self.PBC = [1, 1, 1] self._get_formula() def _from_pymatgen(self, structure): """ Load structure from Pymatgen should not be used directly """ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as sga self.valid = True try: # needs to do it twice in order to get the conventional cell s = sga(structure) structure = s.get_refined_structure() s = sga(structure) sym_struc = s.get_symmetrized_structure() number = s.get_space_group_number() except: print("Failed to load the Pymatgen structure") self.valid = False if self.valid: d = sym_struc.composition.as_dict() species = [key for key in d.keys()] numIons = [] for ele in species: numIons.append(int(d[ele])) self.numIons = numIons self.species = species self.group = Group(number) atom_sites = [] for i, site in enumerate(sym_struc.equivalent_sites): pos = site[0].frac_coords wp = Wyckoff_position.from_group_and_index( number, sym_struc.wyckoff_symbols[i]) specie = site[0].specie.number atom_sites.append(atom_site(wp, pos, specie)) self.atom_sites = atom_sites matrix, ltype = sym_struc.lattice.matrix, self.group.lattice_type self.lattice = Lattice.from_matrix(matrix, ltype=ltype) def check_short_distances(self, r=0.7, exclude_H=True): """ A function to check short distance pairs Mainly used for debug, powered by pymatgen Args: r: the given cutoff distances exclude_H: whether or not exclude the H atoms Returns: list of pairs within the cutoff """ if self.dim > 0: pairs = [] pmg_struc = self.to_pymatgen() if exclude_H: pmg_struc.remove_species('H') res = pmg_struc.get_all_neighbors(r) for i, neighs in enumerate(res): for n in neighs: pairs.append( [pmg_struc.sites[i].specie, n.specie, n.nn_distance]) else: raise NotImplementedError("Does not support cluster for now") return pairs def to_file(self, filename=None, fmt=None, permission='w', sym_num=None): """ Creates a file with the given filename and file type to store the structure. By default, creates cif files for crystals and xyz files for clusters. For other formats, Pymatgen is used Args: filename: the file path fmt: the file type (`cif`, `xyz`, etc.) permission: `w` or `a+` Returns: Nothing. Creates a file at the specified path """ if self.valid: if fmt is None: if self.dim == 0: fmt = 'xyz' else: fmt = 'cif' if fmt == "cif": if self.dim == 3: return write_cif(self, filename, "from_pyxtal", permission, sym_num=sym_num) else: pmg_struc = self.to_pymatgen() if self.molecular: pmg_struc.sort() return pmg_struc.to(fmt=fmt, filename=filename) else: pmg_struc = self.to_pymatgen() if self.molecular: pmg_struc.sort() return pmg_struc.to(fmt=fmt, filename=filename) else: raise RuntimeError( "Cannot create file: structure did not generate") def subgroup(self, H=None, eps=0.05, idx=None, once=False, group_type='t', max_index=4): """ generate a structure with lower symmetry Args: H: space group number (int) eps: pertubation term (float) idx: list once: generate only one structure, otherwise output all Returns: a list of pyxtal structures with lower symmetries """ #randomly choose a subgroup from the available list if group_type == 't': dicts = self.group.get_max_t_subgroup() #['subgroup'] else: dicts = self.group.get_max_k_subgroup() #['subgroup'] Hs = dicts['subgroup'] indices = dicts['index'] if idx is None: idx = [i for i, id in enumerate(indices) if id <= max_index] #idx = range(len(Hs)) else: for id in idx: if id >= len(Hs): raise ValueError( "The idx exceeds the number of possible splits") if H is not None: idx = [id for id in idx if Hs[id] == H] if len(idx) == 0: raise RuntimeError("No subgroup to perform the split") if self.molecular: struc_sites = self.mol_sites else: struc_sites = self.atom_sites sites = [ str(site.wp.multiplicity) + site.wp.letter for site in struc_sites ] valid_splitters = [] bad_splitters = [] for id in idx: splitter = wyckoff_split(G=self.group.number, wp1=sites, idx=id, group_type=group_type) if splitter.valid_split: valid_splitters.append(splitter) else: bad_splitters.append(splitter) if len(valid_splitters) == 0: # do one more step new_strucs = [] for splitter in bad_splitters: trail_struc = self.subgroup_by_splitter(splitter) new_strucs.append( trail_struc.subgroup(once=True, group_type=group_type)) return new_strucs else: if once: return self.subgroup_by_splitter(choice(valid_splitters), eps=eps) else: new_strucs = [] for splitter in valid_splitters: new_strucs.append( self.subgroup_by_splitter(splitter, eps=eps)) return new_strucs 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 apply_perturbation(self, d_lat=0.05, d_coor=0.05, d_rot=1): """ perturb the structure without breaking the symmetry Args: d_coor: magnitude of perturbation on atomic coordinates (in A) d_lat: magnitude of perturbation on lattice (in percentage) """ self.lattice = self.lattice.mutate(degree=d_lat) if self.molecular: for i, site in enumerate(self.mol_sites): site.perturbate(lattice=self.lattice.matrix, trans=d_coor, rot=d_rot) else: for i, site in enumerate(self.atom_sites): site.perturbate(lattice=self.lattice.matrix, magnitude=d_coor) self.source = 'Perturbation' def copy(self): """ simply copy the structure """ return deepcopy(self) def _get_coords_and_species(self, absolute=False, unitcell=True): """ extract the coordinates and species information Args: abosulte: if True, return the cartesian coords otherwise fractional Returns: total_coords (N*3 numpy array) and the list of species """ species = [] total_coords = None if self.molecular: for site in self.mol_sites: coords, site_species = site.get_coords_and_species( absolute, unitcell=unitcell) species.extend(site_species) if total_coords is None: total_coords = coords else: total_coords = np.append(total_coords, coords, axis=0) else: for site in self.atom_sites: species.extend([site.specie] * site.multiplicity) if total_coords is None: total_coords = site.coords else: total_coords = np.append(total_coords, site.coords, axis=0) if absolute: total_coords = total_coords.dot(self.lattice.matrix) return total_coords, species def _get_formula(self): """ A quick function to get the formula. """ formula = "" if self.molecular: numspecies = self.numMols species = [str(mol) for mol in self.molecules] else: numspecies = self.numIons species = self.species for i, s in zip(numspecies, species): formula += "{:s}{:d}".format(s, int(i)) self.formula = formula def to_ase(self, resort=True): """ export to ase Atoms object. """ from ase import Atoms if self.valid: if self.dim > 0: lattice = self.lattice.copy() if self.molecular: coords, species = self._get_coords_and_species(True) latt, coords = lattice.add_vacuum(coords, frac=False, PBC=self.PBC) atoms = Atoms(species, positions=coords, cell=latt, pbc=self.PBC) if resort: permutation = np.argsort(atoms.numbers) atoms = atoms[permutation] return atoms else: coords, species = self._get_coords_and_species() latt, coords = lattice.add_vacuum(coords, PBC=self.PBC) return Atoms(species, scaled_positions=coords, cell=latt, pbc=self.PBC) else: coords, species = self._get_coords_and_species(True) return Atoms(species, positions=coords) else: raise RuntimeError("No valid structure can be converted to ase.") def to_pymatgen(self): """ export to Pymatgen structure object. """ from pymatgen.core.structure import Structure, Molecule if self.valid: if self.dim > 0: lattice = self.lattice.copy() coords, species = self._get_coords_and_species() # Add space above and below a 2D or 1D crystals latt, coords = lattice.add_vacuum(coords, PBC=self.PBC) return Structure(latt, species, coords) else: # Clusters are handled as large molecules coords, species = self._get_coords_and_species(True) return Molecule(species, coords) else: raise RuntimeError( "No valid structure can be converted to pymatgen.") def get_XRD(self, **kwargs): """ compute the PXRD object. ** kwargs include - wavelength (1.54184) - thetas [0, 180] - preferred_orientation: False - march_parameter: None """ return XRD(self.to_ase(), **kwargs) def show(self, **kwargs): """ display the crystal structure """ if self.molecular: from pyxtal.viz import display_molecular return display_molecular(self, **kwargs) else: from pyxtal.viz import display_atomic return display_atomic(self, **kwargs) def optimize_lattice(self): """ optimize the lattice if the cell has a bad inclination angles """ if self.molecular: for i in range(5): lattice, trans, opt = self.lattice.optimize() if opt: for site in self.mol_sites: pos_abs = np.dot(site.position, self.lattice.matrix) pos_frac = pos_abs.dot(lattice.inv_matrix) site.position = pos_frac - np.floor(pos_frac) site.lattice = lattice # for P21/c, Pc, C2/c, check if opt the inclination angle if self.group.number in [7, 14, 15]: for j, op in enumerate(site.wp.ops): vec = op.translation_vector.dot(trans) vec -= np.floor(vec) op1 = op.from_rotation_and_translation( op.rotation_matrix, vec) site.wp.ops[j] = op1 _, perm = Wyckoff_position.from_symops( site.wp.ops, self.group.number) if not isinstance(perm, list): self.diag = True else: self.diag = False self.lattice = lattice else: break # def save(self, filename=None): # """ # Save the structure # Args: # filename: the file to save txt information # """ # dict0 = self.save_dict(db_filename) # with open(filename, "w") as fp: # json.dump(dict0, fp) # # print("save the GP model to", filename, "and database to", db_filename) # # def load(self, filename=None): # """ # load the structure # Args: # filename: the file to save txt information # """ # #print(filename) # with open(filename, "r") as fp: # dict0 = json.load(fp) # self.load_from_dict(dict0, N_max=N_max, device=device) # self.fit(opt=opt) # print("load the GP model from ", filename) # def save_dict(self): """ save the model as a dictionary """ sites = [] if self.molecular: for site in self.mol_sites: sites.append(site.save_dict()) else: for site in self.atom_sites: sites.append(site.save_dict()) dict0 = { "lattice": self.lattice.matrix, "sites": sites, "group": self.group.number, "molecular": self.molecular, "numIons": self.numIons, "numMols": self.numMols, "factor": self.factor, "PBC": self.PBC, "formula": self.formula, "source": self.source, "dim": self.dim, "valid": self.valid, } return dict0 def load_dict(self, dict0): """ load the structure from a dictionary """ self.group = Group(dict0["group"]) self.lattice = Lattice.from_matrix(dict0["lattice"], ltype=self.group.lattice_type) self.molecular = dict0["molecular"] self.number = self.group.number self.factor = dict0["factor"] self.source = dict0["source"] self.dim = dict0["dim"] self.PBC = dict0["PBC"] self.numIons = dict0["numIons"] self.numMols = dict0["numMols"] self.valid = dict0["valid"] self.formula = dict0["formula"] sites = [] if dict0["molecular"]: for site in dict0["sites"]: sites.append(mol_site.load_dict(site)) self.mol_sites = sites else: for site in dict0["sites"]: sites.append(atom_site.load_dict(site)) self.atom_sites = sites