def __init__( self, group, molecules, numMols, volume_factor=1.1, select_high=True, allow_inversion=True, orientations=None, thickness=None, lattice=None, sites = None, conventional = True, tm=Tol_matrix(prototype="molecular"), ): self.dim = 2 self.numattempts = 0 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, sites, conventional, tm, )
def __init__( self, group, molecules, numMols, volume_factor=1.1, select_high=True, allow_inversion=False, orientations=None, area=None, lattice=None, tm=Tol_matrix(prototype="molecular"), seed=None, sites=None, ): self.dim = 1 self.area = area # the effective cross-sectional area in A^2 self.diag = False self.PBC = [0, 0, 1] # The periodic axes of the crystal (1,2,3)->(x,y,z) self.sg = None # The international space group number, not rod groups self.seed = None self.init_common( molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, tm, sites, )
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, conventional=True, diag=False, ): self.dim = 3 # The number of periodic dimensions (1,2,3) self.PBC = [1, 1, 1] self.diag = diag self.selec_high = select_high self.init_common( molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, sites, conventional, tm, )
def check_images( coords, species, lattice, PBC=[1, 1, 1], tm=Tol_matrix(prototype="atomic"), tol=None, d_factor=1.0, ): """ Given a set of (unfiltered) frac coordinates, checks if the periodic images are too close. Args: coords: a list of fractional coordinates species: the atomic species of each coordinate lattice: a 3x3 lattice matrix PBC: the periodic boundary conditions tm: a Tol_matrix object tol: a single override value for the distance tolerances d_factor: the tolerance is multiplied by this amount. Larger values mean atoms must be farther apart Returns: False if distances are too close. True if distances are not too close """ # If no PBC, there are no images to check if PBC == [0, 0, 0]: return True # Create image coords from given coords and PBC coords = np.array(coords) m = create_matrix(PBC=PBC, omit=True) new_coords = [] new_species = [] for v in m: for v2 in coords + v: new_coords.append(v2) new_coords = np.array(new_coords) # Create a distance matrix dm = distance_matrix(coords, new_coords, lattice, PBC=[0, 0, 0]) # Define tolerances if tol is None: tols = np.zeros((len(species), len(species))) for i, s1 in enumerate(species): for j, s2 in enumerate(species): if i <= j: tols[i][j] = tm.get_tol(s1, s2) tols[j][i] = tm.get_tol(s1, s2) tols2 = np.tile(tols, int(len(new_coords) / len(coords))) if (dm < tols2).any(): return False else: return True elif tol is not None: if (dm < tol).any(): return False else: return True return True
def check_mol_sites(ms1, ms2, factor=1.0, tm=Tol_matrix(prototype="molecular")): """ Checks whether or not the molecules of two mol sites overlap. Uses ellipsoid overlapping approximation to check. Takes PBC and lattice into consideration. Args: ms1: a mol_site object ms2: another mol_site object factor: the distance factor to pass to check_distances. (only for inter-atomic distance checking) tm: a Tol_matrix object (or prototype string) for distance checking Returns: False if the Wyckoff positions overlap. True otherwise """ # Get coordinates for both mol_sites c1, _ = ms1.get_coords_and_species() c2, _ = ms2.get_coords_and_species() # Calculate which distance matrix is smaller/faster m_length1 = len(ms1.numbers) m_length2 = len(ms2.numbers) wp_length1 = len(c1) wp_length2 = len(c2) size1 = m_length1 * wp_length2 size2 = m_length2 * wp_length1 # Case 1 if size1 <= size2: coords_mol = c1[:m_length1] # Calculate tol matrix for species pairs tols = np.zeros((m_length1, m_length2)) for i1, number1 in enumerate(ms1.numbers): for i2, number2 in enumerate(ms2.numbers): tols[i1][i2] = tm.get_tol(number1, number2) tols = np.repeat(tols, ms2.wp.multiplicity, axis=1) d = distance_matrix(coords_mol, c2, ms1.lattice.matrix, PBC=ms1.PBC) # Case 2 elif size1 > size2: coords_mol = c2[:m_length2] # Calculate tol matrix for species pairs tols = np.zeros((m_length2, m_length1)) for i1, number1 in enumerate(ms2.numbers): for i2, number2 in enumerate(ms1.numbers): tols[i1][i2] = tm.get_tol(number1, number2) tols = np.repeat(tols, ms1.wp.multiplicity, axis=1) d = distance_matrix(coords_mol, c1, ms1.lattice.matrix, PBC=ms1.PBC) # Check if distances are smaller than tolerances if (d < tols).any(): return False return True
def check_distance( coord1, coord2, species1, species2, lattice, PBC=[1, 1, 1], tm=Tol_matrix(prototype="atomic"), d_factor=1.0, ): """ Check the distances between two set of atoms. Distances between coordinates within the first set are not checked, and distances between coordinates within the second set are not checked. Only distances between points from different sets are checked. Args: coord1: a list of fractional coordinates e.g. [[.1,.6,.4] [.3,.8,.2]] coord2: a list of new fractional coordinates e.g. [[.7,.8,.9], [.4,.5,.6]] species1: a list of atomic species or numbers for coord1 species2: a list of atomic species or numbers for coord2 lattice: matrix describing the unit cell vectors PBC: A periodic boundary condition list, where 1 means periodic, 0 means not periodic. [1,1,1] -> full 3d periodicity, [0,0,1] -> periodicity along the z axis tm: a Tol_matrix object, or a string representing Tol_matrix d_factor: the tolerance is multiplied by this amount. Larger values mean atoms must be farther apart Returns: a bool for whether or not the atoms are sufficiently far enough apart """ # Check that there are points to compare if len(coord1) < 1 or len(coord2) < 1: return True # Create tolerance matrix from subset of tm tols = np.zeros((len(species1), len(species2))) for i1, specie1 in enumerate(species1): for i2, specie2 in enumerate(species2): tols[i1][i2] = tm.get_tol(specie1, specie2) # Calculate the distance between each i, j pair d = distance_matrix(coord1, coord2, lattice, PBC=PBC) if (np.array(d) < np.array(tols)).any(): return False else: return True
def __init__( self, group, species, numIons, factor=1.1, lattice=None, sites=None, tm=Tol_matrix(prototype="atomic", factor=0.7), ): self.dim = 0 self.PBC = [0, 0, 0] self.init_common(species, numIons, factor, group, lattice, sites, False, tm)
def __init__(self, mol=None, symmetrize=True, tm=Tol_matrix(prototype="molecular")): mo = None self.smile = None self.torsionlist = None if type(mol) == str: # Parse molecules: either file or molecule name tmp = mol.split(".") self.name = tmp[0] if len(tmp) > 1: # Load the molecule from the given file if tmp[-1] in ["xyz", "gjf", "g03", "json"]: if os.path.exists(mol): mo = Molecule.from_file(mol) else: raise NameError("{:s} is not a valid path".format(mol)) elif tmp[-1] == 'smi': self.smile = tmp[0] symbols, xyz, self.torsionlist = self.rdkit_mol_init(tmp[0]) mo = Molecule(symbols, xyz) symmetrize = False else: raise NameError("{:s} is not a supported format".format(tmp[-1])) else: # print('\nLoad the molecule {:s} from collections'.format(mol)) mo = molecule_collection[mol] elif hasattr(mol, "sites"): # pymatgen molecule self.name = str(mol.formula) mo = mol if mo is None: msg = "Could not create molecules from given input: {:s}".format(mol) raise NameError(msg) self.props = mo.site_properties if len(mo) > 1: if symmetrize: pga = PointGroupAnalyzer(mo) mo = pga.symmetrize_molecule()["sym_mol"] mo = self.add_site_props(mo) self.mol = mo self.tm = tm self.get_box() self.volume = self.box.volume self.get_radius() self.get_symbols() self.get_tols_matrix() xyz = self.mol.cart_coords self.reset_positions(xyz-self.get_center(xyz))
def __init__( self, group, species, numIons, factor=1.1, area=None, lattice=None, sites=None, tm=Tol_matrix(prototype="atomic"), ): self.dim = 1 self.PBC = [0, 0, 1] self.sg = None self.area = area # the effective cross-sectional area, in A^2, of the unit cell. self.init_common(species, numIons, factor, group, lattice, sites, tm)
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 __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()
class random_crystal: """ Class for storing and generating atomic crystals based on symmetry constraints. Given a spacegroup, list of atomic symbols, the stoichiometry, and a volume factor, generates a random crystal consistent with the spacegroup's symmetry. Args: group: the spacegroup number (1-230), or a `pyxtal.symmetry.Group <pyxtal.symmetry.Group.html>`_ object species: a list of atomic symbols for each ion type, e.g., `["Ti", "O"]` numIons: a list of the number of each type of atom within the primitive cell (NOT the conventional cell), e.g., `[4, 2]` factor (optional): volume factor used to generate the crystal sites (optional): pre-assigned wyckoff sites (e.g., `[["4a"], ["2b"]]`) lattice (optional): `pyxtal.lattice.Lattice <pyxtal.lattice.Lattice.html>`_ object to define the unit cell tm (optional): `pyxtal.tolerance.Tol_matrix <pyxtal.tolerance.Tol_matrix.html>`_ object to define the distances """ 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 self.lattice_attempts = 0 self.coord_attempts = 0 if type(group) != Group: group = Group(group, self.dim) self.init_common(species, numIons, factor, group, lattice, sites, conventional, tm) def __str__(self): if self.valid: s = "------Crystal from {:s}------".format(self.source) s += "\nComposition: {}".format(self.formula) s += "\nDimension: {}".format(self.dim) s += "\nGroup: {} ({})".format(self.group.symbol, self.group.number) s += "\n{}".format(self.lattice) s += "\nWyckoff sites:" 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 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_compatible(self, group, numIons): """ Checks if the number of atoms is compatible with the Wyckoff positions. Considers the number of degrees of freedom for each Wyckoff position, and makes sure at least one valid combination of WP's exists. NOTE Comprhys: Is degrees of freedom used symnomously with multiplicity? perhaps standardising to multiplicity would be clearer? """ # Store whether or not at least one degree of freedom exists has_freedom = False # Store the wp's already used that don't have any freedom used_indices = [] # Loop over species for numIon in numIons: # Get lists of multiplicity, maxn and freedom l_mult0 = [] l_maxn0 = [] l_free0 = [] indices0 = [] for i_wp, wp in enumerate(group): indices0.append(i_wp) l_mult0.append(len(wp)) l_maxn0.append(numIon // len(wp)) if np.allclose(wp[0].rotation_matrix, np.zeros([3, 3])): l_free0.append(False) else: l_free0.append(True) # Remove redundant multiplicities: l_mult = [] l_maxn = [] l_free = [] indices = [] for mult, maxn, free, i_wp in zip(l_mult0, l_maxn0, l_free0, indices0): if free is True: if mult not in l_mult: l_mult.append(mult) l_maxn.append(maxn) l_free.append(True) indices.append(i_wp) elif free is False and i_wp not in used_indices: l_mult.append(mult) indices.append(i_wp) if mult <= numIon: l_maxn.append(1) elif mult > numIon: l_maxn.append(0) l_free.append(False) # Loop over possible combinations p = 0 # Create pointer variable to move through lists # Store the number of each WP, used across possible WP combinations n0 = [0] * len(l_mult) n = deepcopy(n0) for i, mult in enumerate(l_mult): if l_maxn[i] != 0: p = i n[i] = l_maxn[i] break p2 = p if n == n0: return False while True: num = np.dot(n, l_mult) dobackwards = False # The combination works: move to next species if num == numIon: # Check if at least one degree of freedom exists for val, free, i_wp in zip(n, l_free, indices): if val > 0: if free is True: has_freedom = True elif free is False: used_indices.append(i_wp) break # All combinations failed: return False if n == n0 and p >= len(l_mult) - 1: return False # Too few atoms if num < numIon: # Forwards routine # Move p to the right and max out if p < len(l_mult) - 1: p += 1 n[p] = min((numIon - num) // l_mult[p], l_maxn[p]) elif p == len(l_mult) - 1: # p is already at last position: trigger backwards routine dobackwards = True # Too many atoms if num > numIon or dobackwards is True: # Backwards routine # Set n[p] to 0, move p backwards to non-zero, and decrease by 1 n[p] = 0 while p > 0 and p > p2: p -= 1 if n[p] != 0: n[p] -= 1 if n[p] == 0 and p == p2: p2 = p + 1 break if has_freedom: # All species passed: return True return True else: # All species passed, but no degrees of freedom: return 0 return 0 def check_consistency(self, site, numIon): num = 0 for s in site: num += int(s[:-1]) if numIon == num: return True else: msg = "\nThe requested number of atoms is inconsistent: " + str( site) msg += "\nfrom numIons: {:d}".format(numIon) msg += "\nfrom Wyckoff list: {:d}".format(num) raise ValueError(msg) def estimate_volume(self): """ Estimates the volume of a unit cell based on the number and types of ions. Assumes each atom takes up a sphere with radius equal to its covalent bond radius. Returns: a float value for the estimated volume """ volume = 0 for numIon, specie in zip(self.numIons, self.species): r = random.uniform( Element(specie).covalent_radius, Element(specie).vdw_radius) volume += numIon * 4 / 3 * np.pi * r**3 return self.factor * volume def generate_crystal(self): """ The main code to generate a random atomic crystal. If successful, stores a pymatgen.core.structure object in self.struct and sets self.valid to True. If unsuccessful, sets self.valid to False and outputs an error message. """ # Check the minimum number of degrees of freedom within the Wyckoff positions self.numattempts = 1 degrees = self.check_compatible(self.group, self.numIons) if degrees is False: msg = "Warning: the stoichiometry is incompatible with wyckoff choice" printx(msg, priority=1) self.valid = False return if degrees == 0: printx("Wyckoff positions have no degrees of freedom.", priority=2) # NOTE why do these need to be changed from defaults? self.lattice_attempts = 5 self.coord_attempts = 5 #self.wyckoff_attempts = 5 else: self.lattice_attempts = 40 self.coord_attempts = 10 #self.wyckoff_attempts=10 # Calculate a minimum vector length for generating a lattice # NOTE Comprhys: minvector never used? # minvector = max(self.tol_matrix.get_tol(s, s) for s in self.species) for cycle1 in range(self.lattice_attempts): self.cycle1 = cycle1 # 1, Generate a lattice if self.lattice.allow_volume_reset: self.volume = self.estimate_volume() self.lattice.volume = self.volume self.lattice.reset_matrix() try: cell_matrix = self.lattice.get_matrix() if cell_matrix is None: continue # TODO remove bare except except: continue # Check that the correct volume was generated if self.lattice.random: if self.dim != 0 and abs(self.volume - self.lattice.volume) > 1.0: printx( ("Error, volume is not equal to the estimated value: " "{} -> {} cell_para: {}").format( self.volume, self.lattice.volume, self.lattice.get_para), priority=0, ) self.valid = False return # to try to generate atomic coordinates for cycle2 in range(self.coord_attempts): self.cycle2 = cycle2 output = self._generate_coords(cell_matrix) if output: self.atom_sites = output break if self.valid: return return def _generate_coords(self, cell_matrix): """ generate coordinates for random crystal """ wyckoff_sites_list = [] # generate coordinates for each ion type in turn for numIon, specie in zip(self.numIons, self.species): output = self._generate_ion_wyckoffs(numIon, specie, cell_matrix, wyckoff_sites_list) if output is not None: wyckoff_sites_list.extend(output) else: # correct multiplicity not achieved exit and start over return None # If numIon_added correct for all specie return structure self.valid = True return wyckoff_sites_list def _generate_ion_wyckoffs(self, numIon, specie, cell_matrix, wyks): """ generates a set of wyckoff positions to accomodate a given number of ions Args: numIon: Number of ions to accomodate specie: Type of species being placed on wyckoff site cell_matrix: Matrix of lattice vectors wyks: current wyckoff sites Returns: Sucess: wyckoff_sites_tmp: list of wyckoff sites for valid sites Failue: None """ numIon_added = 0 tol = self.tol_matrix.get_tol(specie, specie) tol_matrix = self.tol_matrix wyckoff_sites_tmp = [] # Now we start to add the specie to the wyckoff position sites_list = deepcopy(self.sites[specie]) # the list of Wyckoff site if sites_list is not None: wyckoff_attempts = max(len(sites_list) * 2, 10) else: # the minimum numattempts is to put all atoms to the general WPs min_wyckoffs = int(numIon / len(self.group.wyckoffs_organized[0][0])) wyckoff_attempts = max(2 * min_wyckoffs, 10) cycle = 0 while cycle < wyckoff_attempts: # Choose a random WP for given multiplicity: 2a, 2b if sites_list is not None: site = sites_list[0] else: # Selecting the merging site = None wp = choose_wyckoff(self.group, numIon - numIon_added, site, self.dim) if wp is not False: # Generate a list of coords from ops mult = wp.multiplicity # remember the original multiplicity pt = self.lattice.generate_point() # Merge coordinates if the atoms are close pt, wp, _ = WP_merge(pt, cell_matrix, wp, tol) # For pure planar structure if self.dim == 2 and self.thickness is not None and self.thickness < 0.1: pt[-1] = 0.5 # If site the pre-assigned, do not accept merge if wp is not False: if site is not None and mult != wp.multiplicity: cycle += 1 continue # Use a Wyckoff_site object for the current site new_site = atom_site(wp, pt, specie) # Check current WP against existing WP's passed_wp_check = True for ws in wyckoff_sites_tmp + wyks: if not new_site.check_with_ws2(ws, cell_matrix, tol_matrix): passed_wp_check = False if passed_wp_check: if sites_list is not None: sites_list.pop(0) wyckoff_sites_tmp.append(new_site) numIon_added += new_site.multiplicity # Check if enough atoms have been added if numIon_added == numIon: return wyckoff_sites_tmp cycle += 1 self.numattempts += 1 return None
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()
class random_crystal: """ Class for storing and generating atomic crystals based on symmetry constraints. Given a spacegroup, list of atomic symbols, the stoichiometry, and a volume factor, generates a random crystal consistent with the spacegroup's symmetry. Args: group: the spacegroup number (1-230), or a `pyxtal.symmetry.Group <pyxtal.symmetry.Group.html>`_ object species: a list of atomic symbols for each ion type, e.g., `["Ti", "O"]` numIons: a list of the number of each type of atom within the primitive cell (NOT the conventional cell), e.g., `[4, 2]` factor (optional): volume factor used to generate the crystal sites (optional): pre-assigned wyckoff sites (e.g., `[["4a"], ["2b"]]`) lattice (optional): the `pyxtal.lattice.Lattice <pyxtal.lattice.Lattice.html>`_ object to define the unit cell tm (optional): the `pyxtal.tolerance.Tol_matrix <pyxtal.tolerance.Tol_matrix.html>`_ object to define the distances seed (optional): the cif/POSCAR file from user """ 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() def from_seed(self): """ Load the seed structure from Pymatgen/ASE/POSCAR/CIFs Internally they will be handled by Pymatgen """ self.valid = True from ase import Atoms from pymatgen import Structure if isinstance(self.seed, Atoms): #ASE atoms from pymatgen.io.ase import AseAtomsAdaptor pmg_struc = AseAtomsAdaptor.get_structure(self.seed) self.from_pymatgen(pmg_struc) elif isinstance(self.seed, Structure): #Pymatgen self.from_pymatgen(self.seed) elif isinstance(self.seed, str): pmg_struc = Structure.from_file(self.seed) self.from_pymatgen(pmg_struc) formula = "" for i, s in zip(self.numIons, self.species): formula += "{:s}{:d}".format(s, int(i)) self.formula = formula self.factor = 1.0 self.number = self.group.number self.source = 'Seed' 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 init_common(self, species, numIons, factor, group, lattice, sites, 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 self.numIons0 = numIons self.numIons = self.numIons0 * cellsize(self.group) formula = "" for i, s in zip(self.numIons, species): formula += "{:s}{:d}".format(s, int(i)) self.formula = formula self.species = species self.Msgs() # 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_compatible(self, group, numIons): """ Checks if the number of atoms is compatible with the Wyckoff positions. Considers the number of degrees of freedom for each Wyckoff position, and makes sure at least one valid combination of WP's exists. NOTE Comprhys: Is degrees of freedom used symnomously with multiplicity? perhaps standardising to multiplicity would be clearer? """ # Store whether or not at least one degree of freedom exists has_freedom = False # Store the wp's already used that don't have any freedom used_indices = [] # Loop over species for numIon in numIons: # Get lists of multiplicity, maxn and freedom l_mult0 = [] l_maxn0 = [] l_free0 = [] indices0 = [] for i_wp, wp in enumerate(group): indices0.append(i_wp) l_mult0.append(len(wp)) l_maxn0.append(numIon // len(wp)) if np.allclose(wp[0].rotation_matrix, np.zeros([3, 3])): l_free0.append(False) else: l_free0.append(True) # Remove redundant multiplicities: l_mult = [] l_maxn = [] l_free = [] indices = [] for mult, maxn, free, i_wp in zip(l_mult0, l_maxn0, l_free0, indices0): if free is True: if mult not in l_mult: l_mult.append(mult) l_maxn.append(maxn) l_free.append(True) indices.append(i_wp) elif free is False and i_wp not in used_indices: l_mult.append(mult) indices.append(i_wp) if mult <= numIon: l_maxn.append(1) elif mult > numIon: l_maxn.append(0) l_free.append(False) # Loop over possible combinations p = 0 # Create pointer variable to move through lists # Store the number of each WP, used across possible WP combinations n0 = [0] * len(l_mult) n = deepcopy(n0) for i, mult in enumerate(l_mult): if l_maxn[i] != 0: p = i n[i] = l_maxn[i] break p2 = p if n == n0: return False while True: num = np.dot(n, l_mult) dobackwards = False # The combination works: move to next species if num == numIon: # Check if at least one degree of freedom exists for val, free, i_wp in zip(n, l_free, indices): if val > 0: if free is True: has_freedom = True elif free is False: used_indices.append(i_wp) break # All combinations failed: return False if n == n0 and p >= len(l_mult) - 1: return False # Too few atoms if num < numIon: # Forwards routine # Move p to the right and max out if p < len(l_mult) - 1: p += 1 n[p] = min((numIon - num) // l_mult[p], l_maxn[p]) elif p == len(l_mult) - 1: # p is already at last position: trigger backwards routine dobackwards = True # Too many atoms if num > numIon or dobackwards is True: # Backwards routine # Set n[p] to 0, move p backwards to non-zero, and decrease by 1 n[p] = 0 while p > 0 and p > p2: p -= 1 if n[p] != 0: n[p] -= 1 if n[p] == 0 and p == p2: p2 = p + 1 break # All species passed: return True if has_freedom is True: return True # All species passed, but no degrees of freedom: return 0 elif has_freedom is False: return 0 def check_consistency(self, site, numIon): num = 0 for s in site: num += int(s[:-1]) if numIon == num: return True else: msg = "\nThe requested number of atoms is inconsistent: " + str( site) msg += "\nfrom numIons: {:d}".format(numIon) msg += "\nfrom Wyckoff list: {:d}".format(num) raise ValueError(msg) 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 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 Msgs(self): """ Define a set of error and warning message if generation fails. Returns: nothing """ self.Msg1 = ( "Error: the stoichiometry is incompatible with the wyckoff sites choice" ) self.Msg2 = "Error: failed in the cycle of generating structures" self.Msg3 = "Warning: failed in the cycle of adding species" self.Msg4 = "Warning: failed in the cycle of choosing wyckoff sites" self.Msg5 = "Finishing: added the specie" self.Msg6 = "Finishing: added the whole structure" self.Msg7 = "Error: invalid paramaters for initialization" def estimate_volume(self): """ Estimates the volume of a unit cell based on the number and types of ions. Assumes each atom takes up a sphere with radius equal to its covalent bond radius. Returns: a float value for the estimated volume """ volume = 0 for numIon, specie in zip(self.numIons, self.species): r = random.uniform( Element(specie).covalent_radius, Element(specie).vdw_radius) volume += numIon * 4 / 3 * np.pi * r**3 return self.factor * volume def to_file(self, filename=None, fmt=None, permission='w'): """ 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: from pyxtal.io import write_cif return write_cif(self, filename, "from_pyxtal", permission) else: pmg_struc = self.to_pymatgen() return pmg_struc.to(fmt=fmt, filename=filename) else: pmg_struc = self.to_pymatgen() return pmg_struc.to(fmt=fmt, filename=filename) else: printx("Cannot create file: structure did not generate.", priority=1) def __str__(self): s = "------Crystal from {:s}------".format(self.source) s += "\nComposition: {}".format(self.formula) s += "\nDimension: {}".format(self.dim) s += "\nGroup: {} ({})".format(self.group.symbol, self.group.number) s += "\nVolume factor: {}".format(self.factor) s += "\n{}".format(self.lattice) if self.valid: s += "\nWyckoff sites:" for wyc in self.atom_sites: s += "\n\t{}".format(wyc) else: s += "\nStructure not generated." return s def __repr__(self): return str(self) def show(self, **kwargs): """ display the crystal structure """ from pyxtal.viz import display_atomic return display_atomic(self, **kwargs) def subgroup(self, H=None, eps=0.05, idx=None, once=False): """ 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 Hs = self.group.get_max_t_subgroup()['subgroup'] if idx is None: 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[idx] == H] if len(idx) == 0: raise ValueError("The space group H is incompatible with idx") sites = [ str(site.wp.multiplicity) + site.wp.letter for site in self.atom_sites ] valid_splitters = [] bad_splitters = [] for id in idx: splitter = wyckoff_split(G=self.group.number, wp1=sites, idx=id) 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)) 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): 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 generate_crystal(self): """ The main code to generate a random atomic crystal. If successful, stores a pymatgen.core.structure object in self.struct and sets self.valid to True. If unsuccessful, sets self.valid to False and outputs an error message. """ # Check the minimum number of degrees of freedom within the Wyckoff positions self.numattempts = 1 degrees = self.check_compatible(self.group, self.numIons) if degrees is False: printx(self.Msg1, priority=1) self.valid = False return if degrees == 0: printx("Wyckoff positions have no degrees of freedom.", priority=2) # NOTE why do these need to be changed from defaults? self.lattice_attempts = 5 self.coord_attempts = 5 #self.wyckoff_attempts = 5 else: self.lattice_attempts = 40 self.coord_attempts = 10 #self.wyckoff_attempts=10 # Calculate a minimum vector length for generating a lattice # NOTE Comprhys: minvector never used? # minvector = max(self.tol_matrix.get_tol(s, s) for s in self.species) for cycle1 in range(self.lattice_attempts): self.cycle1 = cycle1 # 1, Generate a lattice if self.lattice.allow_volume_reset: self.volume = self.estimate_volume() self.lattice.volume = self.volume self.lattice.reset_matrix() try: cell_matrix = self.lattice.get_matrix() if cell_matrix is None: continue # TODO remove bare except except: continue # Check that the correct volume was generated if self.lattice.random: if self.dim != 0 and abs(self.volume - self.lattice.volume) > 1.0: printx( ("Error, volume is not equal to the estimated value: " "{} -> {} cell_para: {}").format( self.volume, self.lattice.volume, self.lattice.get_para), priority=0, ) self.valid = False return # to try to generate atomic coordinates for cycle2 in range(self.coord_attempts): self.cycle2 = cycle2 output = self._generate_coords(cell_matrix) if output: self.atom_sites = output break if self.valid: return return def copy(self): """ simply copy the structure """ return deepcopy(self) def _get_coords_and_species(self, absolute=False): """ 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 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: return total_coords.dot(self.lattice.matrix), species else: return total_coords, species def to_ase(self): """ export to ase Atoms object """ from ase import Atoms 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 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: printx("No valid structure can be converted to ase.", priority=1) 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: printx("No valid structure can be converted to pymatgen.", priority=1) def _generate_coords(self, cell_matrix): """ generate coordinates for random crystal """ wyckoff_sites_list = [] # generate coordinates for each ion type in turn for numIon, specie in zip(self.numIons, self.species): output = self._generate_ion_wyckoffs(numIon, specie, cell_matrix, wyckoff_sites_list) if output is not None: wyckoff_sites_list.extend(output) else: # correct multiplicity not achieved exit and start over return None # If numIon_added correct for all specie return structure self.valid = True return wyckoff_sites_list def _generate_ion_wyckoffs(self, numIon, specie, cell_matrix, wyks): """ generates a set of wyckoff positions to accomodate a given number of ions Args: numIon: Number of ions to accomodate specie: Type of species being placed on wyckoff site cell_matrix: Matrix of lattice vectors wyks: current wyckoff sites Returns: Sucess: wyckoff_sites_tmp: list of wyckoff sites for valid sites Failue: None """ numIon_added = 0 tol = self.tol_matrix.get_tol(specie, specie) tol_matrix = self.tol_matrix wyckoff_sites_tmp = [] # Now we start to add the specie to the wyckoff position sites_list = deepcopy(self.sites[specie]) # the list of Wyckoff site if sites_list is not None: wyckoff_attempts = max(len(sites_list) * 2, 10) else: # the minimum numattempts is to put all atoms to the general WPs min_wyckoffs = int(numIon / len(self.group.wyckoffs_organized[0][0])) wyckoff_attempts = max(2 * min_wyckoffs, 10) cycle = 0 while cycle < wyckoff_attempts: # Choose a random WP for given multiplicity: 2a, 2b if sites_list is not None: site = sites_list[0] else: # Selecting the merging site = None wp = choose_wyckoff(self.group, numIon - numIon_added, site, self.dim) if wp is not False: # Generate a list of coords from ops mult = wp.multiplicity # remember the original multiplicity pt = self.lattice.generate_point() # Merge coordinates if the atoms are close pt, wp, _ = WP_merge(pt, cell_matrix, wp, tol) # For pure planar structure if self.dim == 2 and self.thickness is not None and self.thickness < 0.1: pt[-1] = 0.5 # If site the pre-assigned, do not accept merge if wp is not False: if site is not None and mult != wp.multiplicity: cycle += 1 continue # Use a Wyckoff_site object for the current site new_site = atom_site(wp, pt, specie) # Check current WP against existing WP's passed_wp_check = True for ws in wyckoff_sites_tmp + wyks: if not check_atom_sites(new_site, ws, cell_matrix, tol_matrix): passed_wp_check = False if passed_wp_check: if sites_list is not None: sites_list.pop(0) wyckoff_sites_tmp.append(new_site) numIon_added += new_site.multiplicity # Check if enough atoms have been added if numIon_added == numIon: return wyckoff_sites_tmp cycle += 1 self.numattempts += 1 return None
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 init_common( self, molecules, numMols, volume_factor, select_high, allow_inversion, orientations, group, lattice, sites, conventional, tm, ): # 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 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.factor = volume_factor # volume factor for the unit cell. numMols = np.array(numMols) # must convert it to np.array if not conventional: mul = cellsize(self.group) else: mul = 1 self.numMols = numMols * mul # 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 # 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 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()
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