def from_constraints(self, v1, c1, v2, c2): """ Geneate an orientation object given two constraint vectors Args: v1: a 1x3 vector in the original reference frame c1: a corresponding axis which v1 must be mapped to v1: a second 1x3 vector in the original reference frame c1: a corresponding axis which v2 must be mapped to Returns: an orientation object consistent with the supplied constraints """ T = rotate_vector(v1, c1) phi = angle(c1, c2) phi2 = angle(c1, (np.dot(T, v2))) if not np.isclose(phi, phi2, rtol=0.01): printx("Error: constraints and vectors do not match.", priority=1) return r = np.sin(phi) c = np.linalg.norm(np.dot(T, v2) - c2) theta = np.arccos(1 - (c ** 2) / (2 * (r ** 2))) Rot = R.from_rotvec(theta * c1) T2 = np.dot(R, T) a = angle(np.dot(T2, v2), c2) if not np.isclose(a, 0, rtol=0.01): T2 = np.dot(np.linalg.inv(R), T) a = angle(np.dot(T2, v2), c2) if not np.isclose(a, 0, rtol=0.01): printx("Error: Generated incorrect rotation: " + str(theta), priority=1) return Orientation(T2, degrees=0)
def to_file(self, filename=None, fmt="cif", permission='w', **kwargs): """ 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. By default, the filename is based on the stoichiometry. Args: filename: the file path fmt: the file type (`cif`, `xyz`, etc.) permission: `w` or `a+` sym_num: `w` or `a+` Returns: Nothing. Creates a file at the specified path """ if self.valid: if fmt == "cif": if self.dim == 3: return write_cif(self, filename, "from_pyxtal", permission, **kwargs) else: pmg_struc = self.to_pymatgen() pmg_struc.sort() return pmg_struc.to(fmt=fmt, filename=filename) else: pmg_struc = self.to_pymatgen() pmg_struc.sort() return pmg_struc.to(fmt=fmt, filename=filename) else: printx("Cannot create file: structure did not generate.", priority=1)
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 generate_crystal(self): """ The main code to generate a random molecular crystal. If successful, `self.valid` is True (False otherwise) """ # Check the minimum number of degrees of freedom within the Wyckoff positions degrees = self.check_compatible(self.group, self.numMols, self.valid_orientations) if degrees is False: self.valid = False msg = "the space group is incompatible with the number of molecules" #raise ValueError(msg) return else: if degrees == 0: self.lattice_attempts = 20 self.coord_attempts = 3 self.ori_attempts = 1 else: self.lattice_attempts = 40 self.coord_attempts = 30 self.ori_attempts = 5 if not self.lattice.allow_volume_reset: self.lattice_attempts = 1 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() if self._check_lattice_vs_shape(): for cycle2 in range(self.coord_attempts): self.cycle2 = cycle2 output = self._generate_coords() if output: self.mol_sites = output break if self.valid: return printx("Couldn't generate crystal after max attempts.", priority=1) return
def to_pymatgen(self): """ export to Pymatgen structure object """ from pymatgen.core.structure import Structure if self.valid: 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: printx("No valid structure can be converted to pymatgen.", priority=1)
def to_ase(self, resort=True): """ export to ase Atoms object """ from ase import Atoms if self.valid: lattice = self.lattice.copy() 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: printx("No valid structure can be converted to ase.", priority=1)
def check_atom_sites(ws1, ws2, lattice, tm, same_group=True): """ Given two Wyckoff sites, checks the inter-atomic distances between them. Args: ws1: a Wyckoff_site object ws2: a different Wyckoff_site object (will always return False if two identical WS's are provided) lattice: a 3x3 cell matrix same_group: whether or not the two WS's are in the same structure. Default value True reduces the calculation cost Returns: True if all distances are greater than the allowed tolerances. False if any distance is smaller than the allowed tolerance """ # Ensure the PBC values are valid if ws1.PBC != ws2.PBC: printx("Error: PBC values do not match between Wyckoff sites") return # Get tolerance tol = tm.get_tol(ws1.specie, ws2.specie) # Symmetry shortcut method: check only some atoms if same_group is True: # We can either check one atom in WS1 against all WS2, or vice-versa # Check which option is faster if ws1.multiplicity > ws2.multiplicity: coords1 = [ws1.coords[0]] coords2 = ws2.coords else: coords1 = [ws2.coords[0]] coords2 = ws1.coords # Calculate distances dm = distance_matrix(coords1, coords2, lattice, PBC=ws1.PBC) # Check if any distances are less than the tolerance if (dm < tol).any(): return False else: return True # No symmetry method: check all atomic pairs else: dm = distance_matrix(ws1.coords, ws2.coords, lattice, PBC=ws1.PBC) # Check if any distances are less than the tolerance if (dm < tol).any(): return False else: return True
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 orientation_in_wyckoff_position( mol, wyckoff_position, randomize=True, exact_orientation=False, already_oriented=False, allow_inversion=True, rtol=1e-2, ): """ Tests if a molecule meets the symmetry requirements of a Wyckoff position, and returns the valid orientations. Args: mol: a Molecule object. Orientation is arbitrary wyckoff_position: a pyxtal.symmetry.Wyckoff_position object randomize: whether or not to apply a random rotation consistent with the symmetry requirements exact_orientation: whether to only check compatibility for the provided orientation of the molecule. Used within general case for checking. If True, this function only returns True or False already_oriented: whether or not to reorient the principle axes when calling get_symmetry. Setting to True can remove redundancy, but is not necessary allow_inversion: whether or not to allow chiral molecules to be inverted. Should only be True if the chemical and biological properties of the mirror image are known to be suitable for the desired application Returns: a list of operations.Orientation objects which can be applied to the molecule while allowing it to satisfy the symmetry requirements of the Wyckoff position. If no orientations are found, returns False. """ # For single atoms, there are no constraints if len(mol) == 1: return [Orientation([[1, 0, 0], [0, 1, 0], [0, 0, 1]], degrees=2)] wyckoffs = wyckoff_position.ops w_symm = wyckoff_position.symmetry_m # Obtain the Wyckoff symmetry symm_w = w_symm[0] pga = PointGroupAnalyzer(mol) # Check exact orientation if exact_orientation is True: mo = deepcopy(mol) valid = True for op in symm_w: if not pga.is_valid_op(op): valid = False if valid is True: return True elif valid is False: return False # Obtain molecular symmetry, exact_orientation==False symm_m = get_symmetry(mol, already_oriented=already_oriented) # Store OperationAnalyzer objects for each molecular SymmOp chiral = True opa_m = [] for op_m in symm_m: opa = OperationAnalyzer(op_m) opa_m.append(opa) if opa.type == "rotoinversion": chiral = False elif opa.type == "inversion": chiral = False # If molecule is chiral and allow_inversion is False, # check if WP breaks symmetry if chiral is True: if allow_inversion is False: for op in wyckoffs: if np.linalg.det(op.rotation_matrix) < 0: printx( "Warning: cannot place chiral molecule in spagegroup", priority=2, ) return False # Store OperationAnalyzer objects for each Wyckoff symmetry SymmOp opa_w = [] for op_w in symm_w: opa_w.append(OperationAnalyzer(op_w)) # Check for constraints from the Wyckoff symmetry... # If we find ANY two constraints (SymmOps with unique axes), the molecule's # point group MUST contain SymmOps which can be aligned to these particular # constraints. However, there may be multiple compatible orientations of the # molecule consistent with these constraints constraint1 = None constraint2 = None for i, op_w in enumerate(symm_w): if opa_w[i].axis is not None: constraint1 = opa_w[i] for j, op_w in enumerate(symm_w): if opa_w[j].axis is not None: dot = np.dot(opa_w[i].axis, opa_w[j].axis) if (not np.isclose(dot, 1, rtol=rtol)) and (not np.isclose( dot, -1, rtol=rtol)): constraint2 = opa_w[j] break break # Indirectly store the angle between the constraint axes if constraint1 is not None and constraint2 is not None: dot_w = np.dot(constraint1.axis, constraint2.axis) # Generate 1st consistent molecular constraints constraints_m = [] if constraint1 is not None: for i, opa1 in enumerate(opa_m): if opa1.is_conjugate(constraint1): constraints_m.append([opa1, []]) # Generate 2nd constraint in opposite direction extra = deepcopy(opa1) extra.axis = [ opa1.axis[0] * -1, opa1.axis[1] * -1, opa1.axis[2] * -1 ] constraints_m.append([extra, []]) # Remove redundancy for the first constraints list_i = list(range(len(constraints_m))) list_j = list(range(len(constraints_m))) copy = deepcopy(constraints_m) for i, c1 in enumerate(copy): if i in list_i: for j, c2 in enumerate(copy): if i > j and j in list_j and j in list_i: # Check if axes are colinear if np.isclose(np.dot(c1[0].axis, c2[0].axis), 1, rtol=rtol): list_i.remove(j) list_j.remove(j) # Check if axes are symmetrically equivalent else: cond1 = False # cond2 = False for opa in opa_m: if opa.type == "rotation": op = opa.op if np.isclose( np.dot(op.operate(c1[0].axis), c2[0].axis), 1, rtol=5 * rtol, ): cond1 = True break if cond1 is True: # or cond2 is True: list_i.remove(j) list_j.remove(j) c_m = deepcopy(constraints_m) constraints_m = [] for i in list_i: constraints_m.append(c_m[i]) # Generate 2nd consistent molecular constraints valid = list(range(len(constraints_m))) if constraint2 is not None: for i, c in enumerate(constraints_m): opa1 = c[0] for j, opa2 in enumerate(opa_m): if opa2.is_conjugate(constraint2): dot_m = np.dot(opa1.axis, opa2.axis) # Ensure that the angles are equal if abs(dot_m - dot_w) < 0.02 or abs(dot_m + dot_w) < 0.02: constraints_m[i][1].append(opa2) # Generate 2nd constraint in opposite direction extra = deepcopy(opa2) extra.axis = [ opa2.axis[0] * -1, opa2.axis[1] * -1, opa2.axis[2] * -1, ] constraints_m[i][1].append(extra) # If no consistent constraints are found, remove first constraint if constraints_m[i][1] == []: valid.remove(i) copy = deepcopy(constraints_m) constraints_m = [] for i in valid: constraints_m.append(copy[i]) # Generate orientations consistent with the possible constraints orientations = [] # Loop over molecular constraint sets for c1 in constraints_m: v1 = c1[0].axis v2 = constraint1.axis T = rotate_vector(v1, v2) # If there is only one constraint if c1[1] == []: o = Orientation(T, degrees=1, axis=constraint1.axis) orientations.append(o) else: # Loop over second molecular constraints for opa in c1[1]: phi = angle(constraint1.axis, constraint2.axis) phi2 = angle(constraint1.axis, np.dot(T, opa.axis)) if np.isclose(phi, phi2, rtol=rtol): r = np.sin(phi) c = np.linalg.norm(np.dot(T, opa.axis) - constraint2.axis) theta = np.arccos(1 - (c**2) / (2 * (r**2))) # R = aa2matrix(constraint1.axis, theta) R = Rotation.from_rotvec(theta * constraint1.axis).as_matrix() T2 = np.dot(R, T) a = angle(np.dot(T2, opa.axis), constraint2.axis) if not np.isclose(a, 0, rtol=rtol): T2 = np.dot(np.linalg.inv(R), T) o = Orientation(T2, degrees=0) orientations.append(o) # Ensure the identity orientation is checked if no constraints are found if constraints_m == []: o = Orientation(np.identity(3), degrees=2) orientations.append(o) # Remove redundancy from orientations list_i = list(range(len(orientations))) list_j = list(range(len(orientations))) for i, o1 in enumerate(orientations): if i in list_i: for j, o2 in enumerate(orientations): if i > j and j in list_j and j in list_i: # m1 = o1.get_matrix(angle=0) # m2 = o2.get_matrix(angle=0) m1 = o1.matrix m2 = o2.matrix new_op = SymmOp.from_rotation_and_translation( np.dot(m2, np.linalg.inv(m1)), [0, 0, 0]) P = SymmOp.from_rotation_and_translation( np.linalg.inv(m1), [0, 0, 0]) old_op = P * new_op * P.inverse if pga.is_valid_op(old_op): list_i.remove(j) list_j.remove(j) #copies = deepcopy(orientations) orientations_new = [] for i in list_i: orientations_new.append(orientations[i]) #Check each of the found orientations for consistency with the Wyckoff pos. #If consistent, put into an array of valid orientations allowed = [] for o in orientations_new: if randomize is True: op = o.get_op() elif randomize is False: op = o.get_op() #do not change mo = deepcopy(mol) mo.apply_operation(op) if orientation_in_wyckoff_position(mo, wyckoff_position, exact_orientation=True, randomize=False, allow_inversion=allow_inversion): allowed.append(o) if allowed == []: return False else: return allowed
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()
def __init__(self, op): if type(op) == deepcopy(SymmOp): self.op = op """The original SymmOp object being analyzed""" self.tol = op.tol """The numerical tolerance associated with self.op""" self.affine_matrix = op.affine_matrix """The 4x4 affine matrix of the op""" self.m = op.rotation_matrix """The 3x3 rotation (or rotoinversion) matrix, which ignores the translational part of self.op""" self.det = np.linalg.det(self.m) """The determinant of self.m""" elif (type(op) == np.ndarray) or (type(op) == np.matrix): if op.shape == (3, 3): self.op = SymmOp.from_rotation_and_translation(op, [0, 0, 0]) self.m = self.op.rotation_matrix self.det = np.linalg.det(op) else: printx("Error: OperationAnalyzer requires a SymmOp or 3x3 array.", priority=1) # If rotation matrix is not orthogonal if not is_orthogonal(self.m): self.type = "general" self.axis, self.angle, self.order, self.rotation_order = ( None, None, None, None, ) # If rotation matrix is orthogonal else: # If determinant is positive if np.linalg.det(self.m) > 0: self.inverted = False rotvec = Rotation.from_matrix(self.m).as_rotvec() if np.sum(rotvec.dot(rotvec)) < 1e-6: self.axis = None self.angle = 0 else: self.angle = np.linalg.norm(rotvec) self.axis = rotvec / self.angle if np.isclose(self.angle, 0): self.type = "identity" """The type of operation. Is one of 'identity', 'inversion', 'rotation', or 'rotoinversion'.""" self.order = int(1) """The order of the operation. This is the number of times the operation must be applied consecutively to return to the identity operation. If no integer number if found, we set this to 'irrational'.""" self.rotation_order = int(1) """The order of the rotational (non-inversional) part of the operation. Must be used in conjunction with self.order to determine the properties of the operation.""" else: self.type = "rotation" self.order = OperationAnalyzer.get_order(self.angle) self.rotation_order = self.order # If determinant is negative elif np.linalg.det(self.m) < 0: self.inverted = True rotvec = Rotation.from_matrix(-1 * self.m).as_rotvec() if np.sum(rotvec.dot(rotvec)) < 1e-6: self.axis = None self.angle = 0 else: self.angle = np.linalg.norm(rotvec) self.axis = rotvec / self.angle if np.isclose(self.angle, 0): self.type = "inversion" self.order = int(2) self.rotation_order = int(1) else: self.axis *= -1 self.type = "rotoinversion" self.order = OperationAnalyzer.get_order( self.angle, rotoinversion=True) self.rotation_order = OperationAnalyzer.get_order( self.angle, rotoinversion=False) elif np.linalg.det(self.m) == 0: self.type = "degenerate" self.axis, self.angle = None, 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 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 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()