class StructureFactory(object): ''' structure factory contain detecting self-attributes and fabricating new structures Arguments: structure: django's object of structure or structuring objects? ''' def __init__(self, structure): self.__raw_structure = structure self.structure = structure.clone() def zoom(self, scale): """ scale the lattice vector Arguments: scale: coefficient of zoom for lattice parameter Return: object of new structure """ # note that: need to update the volume and volume_pa. check if other parameters also need to update. # method 1 #self.structure.x1 *= scale #self.structure.x2 *= scale #self.structure.x3 *= scale #self.structure.y1 *= scale #self.structure.y2 *= scale #self.structure.y3 *= scale #self.structure.z1 *= scale #self.structure.z2 *= scale #self.structure.z3 *= scale # method 2 lattice = self.structure.lattice self.structure.lattice = lattice * scale self.structure.volume = self.structure.calculatedVolume() return self.structure def addAtom(self, *atoms, **kwargs): """ add some atoms to structure Note: considers translation periodicity of atomic coordinate. Arguments: atom: there are two format of the input for atom parameter: 1. atom object 2. a array contained atomic information [element_symbol, x, y, z, coord_type (optional)]. i.e. ['Na', 0.5, 0.5, 0.5](Direc) or ['Na', 1.3, 3.5, 2.0, 'Cartesian'](Cartesian) kwargs: isConstraint:True/False (default=False). Return: object of new structure """ # check length of atoms if len(atoms) == 0: raise StructureFactoryError("atoms can't be []") for atom in atoms: # check type of atom if isinstance(atom, Atom): if atom.structure == self.__raw_structure: #raise StructureFactoryError("atom already exists in the structure") self.structure.pop() return self.__raw_structure break atom = [atom.element.symbol, atom.x, atom.y, atom.z] # check whether existing this atom on this position if atom[1:] in self.structure: # exists this site self.structure.pop() return self.__raw_structure break else: # check element element_symbol = atom[0] # need to check valid if Element.objects.filter(symbol=element_symbol).count( ) == 0: # nonexistent element in element table InitializeElement(element_symbol) if not (element_symbol in self.structure ): # nonexistent element in the structure self.structure.elements.add( Element.objects.get(symbol=element_symbol)) # position position = normalizingCoordinate( extractingCoordinate(self.structure, atom)) # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] atom = Atom.objects.create( structure=self.structure, element=Element.objects.get(symbol=element_symbol), x=position[0], y=position[1], z=position[2]) poscar = self.structure.formatting(isConstraint=constrained) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def deleteAtom(self, *atoms, **kwargs): """ delete some atoms Arguments: atom: there are two format of the input for atom parameter: 1. atom object 2. a array contained atomic information [element_symbol, x, y, z, coord_type (optional)]. i.e. ['Na', 0.5, 0.5, 0.5](Direc) or ['Na', 1.3, 3.5, 2.0, 'Cartesian'](Cartesian) kwargs: tolerance (default=1e-3): distance tolerance between the position of the input and position of structure, unite is angstrom. isNormalizingCoordinate (default=True): consider the translation periodicity for input of atomic coordinate when calculating the distance, ensure the value is between 0 and 1. i.e. 2.3 -> 0.3. isConstraint:True/False (default=False). Return: object of new structure and status of operating (True/False). """ # check data # check length of atoms if len(atoms) == 0: raise StructureFactoryError("atoms can't be []") for atom in atoms: # check type of atom if isinstance(atom, Atom): if atom.structure != self.__raw_structure: raise StructureFactoryError( "atom doesn't belong the structure") atom = [atom.element.symbol, atom.x, atom.y, atom.z] # tolerance tolerance = 1e-3 if 'tolerance' in kwargs: tolerance = kwargs['tolerance'] # remove translation periodicity isNormalizingCoordinate = True if 'isNormalizingCoordinate' in kwargs: isNormalizingCoordinate = kwargs['isNormalizingCoordinate'] # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] tmpa = self.structure.exists( atom, isNormalizingCoordinate=isNormalizingCoordinate, isConstraint=constrained) if tmpa == None: self.structure.pop() return self.__raw_structure break else: tmpa.delete() poscar = self.structure.formatting(isConstraint=constrained) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def substituteAtom(self, *atoms, **kwargs): """ change type (element or species?) of a atom Arguments: atoms: there are two format of the input for atom parameter: 1. [atom_object, new_element_symbol] 2. [element_symbol, x, y, z, coord_type (optional), new_element_symbol]. i.e. ['Na', 0.5, 0.5, 0.5, 'K'](Direc) or ['Na', 1.3, 3.5, 2.0, 'Cartesian', 'K'](Cartesian) Note: the format of atoms is different with that of atoms in addAtoms and deleteAtoms methods. #atom: there are two format of the input for atom parameter: # 1. atom object # 2. a array contained atomic information [element_symbol, x, y, z, coord_type (optional)]. i.e. ['Na', 0.5, 0.5, 0.5](Direc) or ['Na', 1.3, 3.5, 2.0, 'Cartesian'](Cartesian) kwargs: tolerance (default=1e-3): distance tolerance between the position of the input and position of structure, unite is angstrom. isNormalizingCoordinate (default=True): consider the translation periodicity for input of atomic coordinate when calculating the distance, ensure the value is between 0 and 1. i.e. 2.3 -> 0.3. isConstraint:True/False (default=False). Return: object of new structure """ # check data # check length of atoms if len(atoms) == 0: raise StructureFactoryError("atoms can't be []") for atom in atoms: if len(atom) == 2: element_symbol = atom[1] atom = atom[0] else: element_symbol = atom[-1] atom = atom[:-1] # check type of atom if isinstance(atom, Atom): if atom.structure != self.__raw_structure: raise StructureFactoryError( "atom doesn't belong the structure") atom = [atom.element.symbol, atom.x, atom.y, atom.z] # tolerance tolerance = 1e-3 if 'tolerance' in kwargs: tolerance = kwargs['tolerance'] # remove translation periodicity isNormalizingCoordinate = True if 'isNormalizingCoordinate' in kwargs: isNormalizingCoordinate = kwargs['isNormalizingCoordinate'] # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] tmpa = self.structure.exists( atom, isNormalizingCoordinate=isNormalizingCoordinate, isConstraint=constrained) if tmpa == None: self.structure.pop() return self.__raw_structure break else: # check element_symbol if Element.objects.filter(symbol=element_symbol).count( ) == 0: # nonexistent element in element table InitializeElement(element_symbol) # add element to structure self.structure.elements.add( Element.objects.get(symbol=element_symbol)) self.structure.save() # substitute # method 1 (1.create new atom, 2. delete old atom) atom = Atom.objects.create( structure=self.structure, element=Element.objects.get(symbol=element_symbol), x=tmpa.position[0], y=tmpa.position[1], z=tmpa.position[2]) tmpa.delete() atom.save() # method 2 (modify atom.element) # atom_set[index].element=Element.objects.get(symbol=element_symbol) # atom_set[index].save() poscar = self.structure.formatting(isConstraint=constrained) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def vacuum(self, direction, **kwargs): """ add vacuum along a direction arguments: direction: direction vector to add the vacuum along lattice vector(a/b/c) (unit: Angstroms). i.e. [2, 0, 0] kwargs: isCenter: centroid of all atoms in structure (True/False). The default value is True. distance: moving distance for all atoms. i.e. 2. Note that distance <= modulus of direction vector isConstraint:True/False (default=False). Return: object of new structure """ # check direction, ensure to exist two zero and one non-zero. if len(direction) != 3: raise StructureFactoryError('invalid dimension of direction!') # method 1 #dcounter={x:direction.count(x) for x in direction} #if dcounter[0] > 2: # method 2 dsum = np.sum(direction) # sum of direction dabsum = np.sum( np.absolute(direction)) # sum of absolute value of direction if (dsum != dabsum) or dabsum == 0: raise StructureFactoryError('invalid direction!') isCenter = True # default if 'isCenter' in kwargs: isCenter = kwargs['isCenter'] if 'distance' in kwargs: raise StructureFactoryError( 'conflicts between isCenter and distance parameters! only exits one.' ) distance = 0 if 'distance' in kwargs: distance = kwargs['distance'] # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] # transform coordinate of direction to cartesian. lattice = self.structure.lattice lattice_parameters = self.structure.lattice_parameters dtmp = [] for i in xrange(0, len(direction)): if direction[i] != 0: # vacuum direction dtmp = np.array( lattice[i] * direction[i] / lattice_parameters[i]) # new translation direction # move atoms atom_set = self.structure.atoms.all() atom_position_set = { } # temporary save the new position of atom. {atom:new_position(cartesian)} atom_position_set2 = { } # temporary save the new position of atom which its component equal 0 along vacuum direction. for j in xrange(0, atom_set.count()): atom = atom_set[j] atom_position_set[atom] = atom.direct2cartesian() if atom.position[i] == 0: position = atom.position position[i] = 1 atmp = Atom.objects.create(structure=self.structure, element=atom.element, x=position[0], y=position[1], z=position[2]) atom_position_set2[atmp] = atom.direct2cartesian( ) + lattice[i] # change lattice scale = (self.structure.lattice_parameters[i] + direction[i]) / (self.structure.lattice_parameters[i]) if i == 0: # a self.structure.x1 = (self.structure.x1) * scale self.structure.y1 = (self.structure.y1) * scale self.structure.z1 = (self.structure.z1) * scale elif i == 1: # b self.structure.x2 = (self.structure.x2) * scale self.structure.y2 = (self.structure.y2) * scale self.structure.z2 = (self.structure.z2) * scale elif i == 2: # c self.structure.x3 = (self.structure.x3) * scale self.structure.y3 = (self.structure.y3) * scale self.structure.z3 = (self.structure.z3) * scale self.structure.save() # change atomic position # distance if 'distance' in kwargs: scale = distance / (self.structure.lattice_parameters[i] + direction[i]) for atom, position in atom_position_set.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position + dtmp * scale)) for atom, position in atom_position_set2.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position + dtmp * scale)) # isCenter elif isCenter: for atom, position in atom_position_set.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position + dtmp / 2)) for atom, position in atom_position_set2.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position + dtmp / 2)) else: # isCenter=False, atom don't move for atom, position in atom_position_set.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position)) for atom, position in atom_position_set2.iteritems(): atom.position = np.array( cartesian2direct(self.structure, position)) poscar = self.structure.formatting(isConstraint=constrained) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def magnetismOrder(self, elements): """ At present, only consider FM configuration. Other magnetic configuration need to set the atomic magnetism one by one. Arguments elements: dict of element's symbol and its magnetic moment. {'Fe': 5, 'Cr': 3, ...} """ for k in elements: if self.structure.exists(k) == None: raise StructureFactoryError('nonexistent element') for atom in self.structure.atoms.all(): if atom.element.symbol in elements: atom.magmom = elements[atom.element.symbol] else: atom.magmom = 0 atom.save() return self.structure def constraint(self, *atoms, **kwargs): """ selected dynamics Arguments: atom: there are two format of the input for atom parameter: 1. [atom_object, constraint] 2. a array contained atomic information [element_symbol, x, y, z, coord_type (optional), constraint]. i.e. ['Na', 0.5, 0.5, 0.5, [True, True, False]](Direc) or ['Na', 1.3, 3.5, 2.0, 'Cartesian', [True, True, False]](Cartesian) constraint: constraint of atomic position (boolean value). i.e. [True, True, False] kwargs: tolerance: distance tolerance between the position of the input and position of structure, unite is angstrom. isNormalizingCoordinate (default=True): consider the translation periodicity for input of atomic coordinate when calculating the distance, ensure the value is between 0 and 1. i.e. 2.3 -> 0.3. isConstraint:True/False (default=False). Return: object of new structure and status of operating (True/False). """ # check data # check length of atoms if len(atoms) == 0: raise StructureFactoryError("atoms can't be []") for atom in atoms: if len(atom) == 2: constraint = atom[1] atom = atom[0] else: constraint = atom[-1] atom = atom[:-1] # check type of atom if isinstance(atom, Atom): if atom.structure != self.__raw_structure: raise StructureFactoryError( "atom doesn't belong the structure") tom = [atom.element.symbol, atom.x, atom.y, atom.z] # tolerance tolerance = 1e-3 if 'tolerance' in kwargs: tolerance = kwargs['tolerance'] # remove translation periodicity isNormalizingCoordinate = True if 'isNormalizingCoordinate' in kwargs: isNormalizingCoordinate = kwargs['isNormalizingCoordinate'] # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] tmpa = self.structure.exists( atom, isNormalizingCoordinate=isNormalizingCoordinate, isConstraint=constrained) if tmpa == None: self.structure.pop() return self.__raw_structure break else: tmpa.cx = constraint[0] tmpa.cy = constraint[1] tmpa.cz = constraint[2] tmpa.save() poscar = self.structure.formatting(isConstraint=True) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def redefine(self, operatorMatrix): """ redefine lattice cell. C'=C x M Arguments: operatorMatrix: operator matrix (i.e. M). M=[[0, 1, 1], [1, 0, 1], [1, 1, 0]] Note: the component of M should be integer. And the volume of M is an integer greater than 0. Return: object of structure and status of operating """ # check operatorMatrix tmpm = np.array(operatorMatrix) if not ((len(tmpm.shape) == 2) and ((tmpm[0] == 3) and (tmpm[1] == 3))): raise StructureFactoryError( 'invalid dimension of operatorMatrix (3x3)') for component in np.nditer(tmpm): if not (isinstance(component, int)): raise StructureFactoryError( "component of operatorMatrix isn't an integer") volume = np.linalg.det(tmpm) if not (isinstance(volume, int)): raise StructureFactoryError( "volume of operatorMatrix isn't an integer") def __toPOSCAR(self, cell): """ convert cell to poscar """ lattice = cell[0] # elements, numbers and position atom_set = OrderedDict() # elements and its positions for i in xrange(0, len(cell[2])): e = Element.objects.filter(z=cell[2][i])[0].symbol if e in atom_set: tmp = atom_set[e] tmp.append(cell[1][i].tolist()) atom_set[e] = tmp else: atom_set[e] = [cell[1][i].tolist()] elements = [] numbers = [] positions = [] for k in atom_set.keys(): elements.append(k) numbers.append(len(atom_set[k])) if positions == []: positions = atom_set[k] else: positions = np.vstack((positions, atom_set[k])) elements = np.array(elements) numbers = np.array(numbers) # poscar=(comment,lattice,elements,numbers,type,positions,constraints) poscar = { 'lattice': lattice, 'elements': elements, 'numbers': numbers, 'type': 'Direct', 'positions': positions } return poscar def refine(self, symprec=1e-5, angle_tolerance=-1.0): """ refine structure which can change the cell's shape Arguments: symprec (symmetry tolerance): distance tolerance in Cartesian coordinates to find crystal symmetry angle_tolerance: An experimental argument that controls angle tolerance between basis vectors. Normally it is not recommended to use this argument. """ cell = self.structure.formatting('cell') cell = (cell['lattice'], cell['positions'], cell['numbers']) newcell = spglib.refine_cell(cell, symprec=symprec, angle_tolerance=angle_tolerance) if newcell == None: raise StructureFactoryError('the search is filed') else: poscar = self.__toPOSCAR(newcell) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def primitive(self, symprec=1e-5): """ primitive structure Arugments: symprec (symmetry tolerance): distance tolerance in Cartesian coordinates to find crystal symmetry """ cell = self.structure.formatting('cell') cell = (cell['lattice'], cell['positions'], cell['numbers']) newcell = spglib.find_primitive(cell, symprec=symprec) if newcell == None: raise StructureFactoryError('the search is filed') else: poscar = self.__toPOSCAR(newcell) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def conventional(self, symprec=1e-5): """ conventional structure Arugments: symprec (symmetry tolerance): distance tolerance in Cartesian coordinates to find crystal symmetry """ cell = self.structure.formatting('cell') cell = (cell['lattice'], cell['positions'], cell['numbers']) # method 1 lattice, scaled_positions, numbers = spglib.standardize_cell( cell, symprec=symprec) newcell = (lattice, scaled_positions, numbers) # method 2 #newcell=spglib.standardize_cell(cell, symprec=symprec) if newcell == None: raise StructureFactoryError('The search is failed') else: poscar = self.__toPOSCAR(newcell) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def niggliReduce(self, eps=1e-5): """ Niggli reduction Arguments: eps: tolerance parameter, but unlike symprec the unit is not a length. This is used to check if difference of norms of two basis vectors is close to zero or not and if two basis vectors are orthogonal by the value of dot product being close to zero or not. The detail is shown at https://atztogo.github.io/niggli/. """ cell = self.structure.formatting('cell') lattice = cell['lattice'] niggli_lattice = spglib.niggli_reduce(lattice, eps=eps) return niggli_lattice def delaunayReduce(self, eps=1e-5): """ Delaunay reduction Arguments: eps: tholerance parameter, see niggliReduce. """ cell = self.structure.formatting('cell') lattice = cell['lattice'] delaunay_lattice = spglib.delaunay_reduce(lattice, eps=eps) return delaunay_lattice def supercell(self, dim): """ create supercell Arguments: dim: size of supercell. i.e. [2,2,2] Return: object of supercell structure. """ # step: # 1. move atom # 2. change the lattice vector # 3. call formatting # 4. pop old # 5. append new # check dim if (sum(1 for x in dim if x <= 0) >= 1) or (sum( 1 for x in dim if not isinstance(x, int)) >= 1): raise StructureFactoryError('invalid dim') atom_set = self.structure.atoms.all() for i in xrange(0, dim[0]): # x for j in xrange(0, dim[1]): # y for k in xrange(0, dim[2]): # z for atom in atom_set: Atom.objects.create(structure=self.structure, element=atom.element, x=(atom.x + i) / dim[0], y=(atom.y + j) / dim[1], z=(atom.z + k) / dim[2]) # delete old atoms for atom in atom_set: atom.delete() lattice = self.structure.lattice for i in xrange(0, lattice.shape[0]): lattice[i] = lattice[i] * dim[i] self.structure.lattice = lattice self.structure.save() #poscar=self.structure.formatting(isConstraint=constrained) poscar = self.structure.formatting(isConstraint=False) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def alloy(self): """ create alloy """ pass def surface(self): """ """ def adsorption(self): """ adsorption on a surface """ pass def rotation(self, axis, angle, angle_type="Radian", **kwargs): """ rotate selected atoms set Arguments: axis: rotating axis [x, y, z]. angle: rotating angle. angle_type: type of angle (Radian(default)/Degree). kwargs: isConstraint:True/False (default=False). Return: object of new structure """ axis = axis / np.linalg.norm(axis) rx = axis[0] ry = axis[1] rz = axis[2] type = 'Radian' if angle_type.lower().startswith('d'): type = 'Degree' angle = np.deg2rad(angle) elif angle_type.lower().startswith('r'): type = 'Radian' else: raise StructureFactoryError( 'unknown value in angle_type(Radian/Degree)!') # constraint constrained = False # whether constrain atom if 'isConstraint' in kwargs: constrained = kwargs['isConstraint'] sina = np.sin(angle) cosa = np.cos(angle) #set the transition matrix r_matrix = np.matrix([[ cosa + (1 - cosa) * np.square(rx), (1 - cosa) * rx * ry - rz * sina, (1 - cosa) * rx * rz + ry * sina ], [(1 - cosa) * rx * ry + rz * sina, cosa + (1 - cosa) * np.square(ry), (1 - cosa) * ry * rz - rx * sina], [(1 - cosa) * rx * rz - ry * sina, (1 - cosa) * ry * rz + rx * sina, cosa + (1 - cosa) * np.square(rz)]]) #rotate the atoms for atom in self.structure.atoms.all(): position = atom.position position = (position * r_matrix).tolist()[0] atom.x = position[0] atom.y = position[1] atom.z = position[2] atom.save() poscar = self.structure.formatting(isConstraint=constrained) self.structure.pop() # delete old object self.structure = Structure().append(poscar) return self.structure def translation(self, atom_set, destination): """ translation selected atoms set Arguments: atom_set: collection of operated atoms [[element_symbol1, x1, y1, z1, coord_type1 (optional; default=Direct)], [element_symbol2, x2, y2, z2, coord_type2 (optional; default=Direct)], ...]] ##vector: distance of translation(unit: Angstroms). i.e. [5.0, 0.0, 0.0] destination: position of moving destination. for crystal: [x, y, z, coord_type (optional; default=Direct)] for molecule: [x, y, z] # only Cartesian """ if isinstance(self.structure, Structure): # crystal if (len(destination) == 3) or ( (len(destination) == 4) and destination.lower().startswith('d')): # direct dposition = direct2cartesian( self.structure, destination) # position of destination elif isinstance(self.structure, MolStructure): # molecule pass def images4NEB(self, struc2, nstruc, **kwargs): """ image structures for neb calculation Note that: beware whether the atomic orders between two structures are consistent. Arguments: struc2: the last structure nstruc: number of images(including the two endpoint structures) kwargs: isConstraint (default=True):True/False isTranslation (default=True): consider the translation periodicity when calculating the distance Return: a list of all images (structures). """ if not ((self.structure.lattice.tolist() == struc2.lattice.tolist()) or (self.structure.natoms == struc2.natoms)): raise StructureFactoryError( 'the two structures are mismatched, check lattice vectors or number of atoms' ) isConstraint = True # default if 'isConstraint' in kwargs: isConstraint = kwargs['isConstraint'] isTranslation = True if 'isTranslation' in kwargs: isTranslation = kwargs['isTranslation'] atom_set1 = self.structure.atoms.all() # atoms of the start structure atom_set2 = struc2.atoms.all() # atoms of the end structure struc_set = [] # output structure list dvector_set = [ ] # atomic displacement (delta vector) for every images from the start structure to the end structure # vectors per step for i in xrange(0, len(atom_set1)): vector = [(atom_set2[i].x - atom_set1[i].x), (atom_set2[i].y - atom_set1[i].y), (atom_set2[i].z - atom_set1[i].z)] # translation periodicity if isTranslation: for j in xrange(0, len(vector)): if vector[j] > 0.5: vector[j] -= 1 elif vector[j] < -0.5: vector[j] += 1 dvector = np.array(vector) / (nstruc - 1) dvector_set.append(dvector) # append the start structure struc_set.append(self.structure) # append the following structures tmps = self.structure.clone() # temporary structure for i in xrange(1, nstruc - 1): atom_index = 0 for atom in tmps.atoms.all(): atom.x += dvector_set[atom_index][0] atom.y += dvector_set[atom_index][1] atom.z += dvector_set[atom_index][2] atom.save() atom_index += 1 poscar = tmps.formatting(isConstraint=isConstraint) struc_set.append(Structure().append(poscar)) tmps.pop() # delete temporary structure # append the end structure struc_set.append(struc2) return struc_set def defect(self): """ """ pass def interface(self): """ """ pass