def solve_sextic(Cij, n=cry.ei(1), m=cry.ei(2)): '''Finds the eigenvalues and eigenvectors of the N tensor in the Stroh sextic eigenvalue formulation of anisotropic elasticity. Rearranges roots p and vectors A and L to have roots with Im(p) > 0 listed first. ''' # Index list used to list eigenvalues with Im(p) > 0 first orderedIndices = np.array([0, 2, 4, 1, 3, 5]) p, xsi = lin.eig(NStroh(Cij, n, m)) # number of eigenvalues of N nEig = len(p) # reorder p p = np.array([p[i] for i in orderedIndices]) # Extract the (non-normalised) A and L vectors, noting that numpy.linalg # returns the i-th eigenvector as xsi[:, i]. Ensure that order is same as # for pOrdered (tilde denotes non-normalised) ATilde = np.array([xsi[:3, i] for i in orderedIndices]) LTilde = np.array([xsi[3:, i] for i in orderedIndices]) # normalise A and L so that 2*(Ai.Li) = 1 # begin by working out values of 2*(ATilde_i . LTilde_i) normalisation = np.array([1./cmath.sqrt(2*np.dot(ATilde[i], LTilde[i])) for i in range(nEig)]) # now multiply each of the ATilde_i and LTilde_i by a normalization factor A = np.array([normalisation[i]*ATilde[i] for i in range(nEig)]) L = np.array([normalisation[i]*LTilde[i] for i in range(nEig)]) return p, A, L
def __init__(self,a=cry.ei(1),b=cry.ei(2),c=cry.ei(3)): '''Identical to <__init__> for <cry.Crystal>, but with the <__init__> call for the basis done using class <CastepBasis>. ''' cry.Lattice.__init__(self,a,b,c) CastepBasis.__init__(self)
def anisotropic_K_b(Cij, b, n=cry.ei(1), m=cry.ei(2), using_atomic=True): '''Caluculate the energy coefficient for a dislocation with a specific Burgers vector. ''' # solve sextic eigenvalue problem p, A, L = aniso.solve_sextic(Cij, n, m) energy_tensor = aniso.tensor_k(L) # calculate the scalar energy coefficient K = aniso.scalar_k(energy_tensor, b) return K
def gamma_surface(slab, resolution, write_fn, sys_info, basename='gsf', suffix='in', limits=(1, 1), vacuum=0., mkdir=False, relax=None): '''Sets up gamma surface calculation with a sampling density of <N> along [100] and <M> along [010]. Note: if N and M have been determined using <gs_sampling>, they will already be even but, for transferability, we nevertheless check that this condition is met. ''' # using <gs_sampling>, calculate the number of increments along x and y # required to give *at least* the specified <resolution>. N, M = gs_sampling(slab.getLattice(), resolution, limits) # iterate over displacement vectors in the gamma surface (ie. (001)) for n in range(0, N + 1): for m in range(0, M + 1): gsf_name = '{}.{}.{}'.format(basename, n, m) # insert vector into slab disp_vec = cry.ei(1) * n * limits[0] / float(N) + cry.ei( 2) * m * limits[1] / float(M) insert_gsf(slab, disp_vec, vacuum=vacuum) # write to code appropriate output file if mkdir: # make directory if not os.path.exists(gsf_name): os.mkdir(gsf_name) outstream = open('{}/{}.{}'.format(gsf_name, gsf_name, suffix), 'w') else: # use current directory outstream = open('{}.{}'.format(gsf_name, suffix), 'w') write_fn(outstream, slab, sys_info, to_cart=False, defected=True, do_relax=True, add_constraints=True, relax_type=relax, prop=False) return
def write_constraints(self, write_fn): '''Write constraints to <write_fn> (usually a file I/O stream) in the CASTEP format, listing the number of the atom in the list of all atoms contained in the basis (<n_atom>), as well as which number of atom of this particular species we are writing to output. ''' if type(write_fn) is file: write_fn = write_fn.write end = '\n' else: end = '' # test to see if any atom actually has constraints use_constraints = False for atom in self: if atom.has_constraints: use_constraints = True self._currentindex = 0 break if not use_constraints: # no need to write an ionic constraints block return # otherwise, write the ionic constraints block to <write_fn> write_fn('%BLOCK IONIC_CONSTRAINTS' + end) n_constraints = 0 # tracks number of species (+1) for which constraints have been written species_j = 1 # track number of each type of atom for which species have been written species_k = 1 for i, atom in enumerate(self): if i == 0: current_species = atom.getSpecies() elif atom.getSpecies() == current_species: species_k += 1 else: # next species, reset counter for number of atoms of given type # and the identity of the current type of atom considered species_j += 1 species_k = 1 current_species = atom.getSpecies() # test for constraints for i, const in enumerate(atom.get_constraints()): if int(const) == 0: # fix atom in place # note that, in castep, 1 implies that motion is constrained fix = cry.ei(i+1) write_fn('{:.0f} {} {:.0f} {:.2f} {:.2f} {:.2f}{}'.format( species_j, atom.getSpecies(), species_k, fix[0], fix[1], fix[2], end)) write_fn("%ENDBLOCK IONIC_CONSTRAINTS" + end + end) return
def makeAnisoField(Cij, n=cry.ei(1), m=cry.ei(2)): '''Given an elastic constants matrix <Cij>, defines a function <uAniso> that returns the displacement at <x> for a dislocation with Burgers vector <b>. ''' # calculate eigenvalues <p>, and the <A> and <L> vectors defined on pg. 468 # of Hirth and Lothe (1982). p, A, L = solve_sextic(Cij) def uAniso(x, b, x0, dummy1=0, dummy2=0): '''Dummy variables used to ensure that all displacement fields have the same form. ''' u = 0j*np.zeros(3) dx = x[0] - x0[0]-1e-10 dy = x[1] - x0[1] rho2 = dx**2 + dy**2 # make sure that we are not at the line singularity of the dislocation coreradius2 = 1e-10 if (rho2 < coreradius2): # inside the dislocation core u[0] = 0. u[1] = 0. u[2] = 0. else: # calculate displacement associated with each conjugate pair of # eigenvalues/eigenvectors. for i in range(3): # original version posEig = A[i]*np.dot(L[i], b)*np.log(dy+p[i]*dx) negEig = A[i+3]*np.dot(L[i+3], b)*np.log(dy+p[i+3]*dx) u = u + (negEig-posEig).copy() # make real u *= 1/(2.*np.pi*1j) return u.real return uAniso
def gl_sampling(lattice, resolution=0.25, vector=cry.ei(1), limits=1.): '''Determine the number of samples along <vector> required to achieve specified <resolution>. ''' # transform Burgers vector in cartesian coordinates burgers = np.zeros(3) for i in range(3): burgers += vector[i] * lattice[i] # get minimum number of samples required for desired resolution N = ceiling(abs(limits) * norm(burgers) / resolution) N = int(N) # Make sure that N is an even integer if N % 2 == 1: N = N + 1 print("Incrementing N to make value even. New value is {}.".format(N)) return N
def insert_gsf(slab, disp_vec, vacuum=0., eps=1e-6): '''Inserts generalised stacking fault with stacking fault vector <disp_vec> into the provided <slab>. If <vacuum> == 0., the stacking fault is inserted at z = 0.5, otherwise, we insert it at 0.5*(z-vacuum)/z (z := slab height). ''' height = norm(slab.getC()) # disp_vec may be entered in 2D, but atomic coordinates are 3D if len(disp_vec) == 3: disp_vec = np.array(disp_vec) elif len(disp_vec) == 2: print("Converting displacement vector in R^{2} into a vector in R^{3}") temp = np.copy(disp_vec) disp_vec = np.array([temp[0], temp[1], 0.]) else: raise ValueError("<disp_vec> has invalid (%d) # of dimensions" % len(disp_vec)) # find the middle of the slab of atoms middle = 0.5 * (height - vacuum) / norm(height) # record whether or not a small perturbation has been applied perturbed = False for atom in slab.getAtoms(): if 0. <= atom.getCoordinates()[-1] < middle - eps: # atom in the lower half of the cell -> no slip continue else: # displace atom x0 = atom.getCoordinates() if not perturbed: new_x = (x0 + disp_vec + cry.ei(3) * 0.01) % 1 else: new_x = (x0 + disp_vec) % 1 atom.setDisplacedCoordinates((x0 + disp_vec) % 1) return
def command_line_options(): '''Parse command line options to enable optional features and change default values of parameters. Example use: $: ./gsf_controller -u example_file.gin -sn example -n 10 -g $GULP/Src/./gulp -> defaults to a gamma surface calculation with a minimum resolution of 1 node per 0.25 x 0.25 square (in \AA, bohr, or whatever other distance units are chosen. ''' options = argparse.ArgumentParser() options.add_argument('-c', '--control-file', type=str, dest='control', default='', help='File containing simulation parameters') options.add_argument('-u', '--unit-cell', type=str, dest='cell_name', help='Name of GULP file containing the unit cell') options.add_argument('-sn', '--name', type=str, dest='sim_name', default='gsf', help='Base name for GSF calculation input files') options.add_argument('-n', '--num-layers', type=int, dest='n', default=2, help='Thickness of simulation slab in unit-cells.') options.add_argument('-v', '--vacuum', type=float, dest='vac', default=0.0, help='Thickness of the vacuum layer.') options.add_argument('-p' '--prog', type=str, dest='prog', default=None, help='Name of atomistic program used. Options:\n' + 'GULP\n' + 'Quantum Espresso (QE)\n' 'CASTEP') options.add_argument('-exe', '--executable', type=str, dest='progexec', default=None, help='Path to the executable for the atomistic code.') options.add_argument('-t', '--type', type=str, choices=['gline', 'gsurface'], dest='simulation_type', default='gsurface', help='Choose whether to calculate the PES of a gamma' + ' line or a gamma surface.\n\nDefault is gamma ' + ' surface.') options.add_argument('-d', '--direction', nargs=3, type=float, dest='line_vec', default=cry.ei(1), help='Direction of gamma line.') options.add_argument('-r', '--resolution', type=float, dest='res', default=0.25, help='Sampling resolution of the gamma line/surface') options.add_argument('-s', '--shift', type=float, dest='shift', default=0.0, help='Shifts origin of unit cell.') # limit the gamma surface/line calculation to one sector of the slip plane. # Only x and y limits are available through the command line and manual input. # For all other limits (in particular by angle), use the input file (not yet # implemented). options.add_argument('-x', '--xmax', type=float, dest='max_x', default=1.0, help='Maximum displacement vector along x.') options.add_argument('-y', '--ymax', type=float, dest='max_y', default=1.0, help='Maximum displacement vector along y.') # list of contraints options.add_argument('-fx', '--dfix', type=float, dest='d_fix', default=5.0, help='Thickness of static region in vacuum-buffered slab.') options.add_argument('-fr', '--free', type=str, nargs = '*', dest='free_atoms', help='List of atomic species allowed to relax without constraint.', default=[]) return options
def bond_candidates_cluster(dis_cell, atom_type, max_bond_length, R, RI, RII, use_species=False, bonded_type=None): '''Extracts candidate bonds for all sites of the specified type with radius R. ''' if bonded_type is None: # calculate Nye tensor on the <atom_type> sublattice bonded_type = atom_type # read in file containing the relaxed dislocation structure, then extract # atoms in the relaxed region, as well as the cell thickness discluster, sinfo = gulp.cluster_from_grs(dis_cell, RI, RII) relaxed = discluster.getRegionIAtoms() H = discluster.getHeight() n = len(relaxed) # extract a list of potential bonds for atoms in the sublattice of interest Qpot = dict() for i in range(n): # check that atom i belongs to the sublattice of interest (usually not # oxygen) if use_species: atomspecies = relaxed[i].getSpecies() if atomspecies != atom_type: continue # ensure that atom i is within the specified region x = relaxed[i].getCoordinates() if (np.sqrt(x[0]**2 + x[1]**2)) > R: continue Qpoti = [] for j in range(n): # check that atom j is one whose bonds with atom i we care about if use_species: if relaxed[j].getSpecies() != bonded_type: continue # extract coordinates of atom j and its periodic images y0 = relaxed[j].getCoordinates() ydown = y0 - cry.ei(3) * H yup = y0 + cry.ei(3) * H # calculate distance to atom i and check to see if bond length is # below given maximum value if i != j and norm(x - y0) < max_bond_length: Qpoti.append([j, x - y0]) if norm(x - yup) < max_bond_length: Qpoti.append([j, x - yup]) if norm(x - ydown) < max_bond_length: Qpoti.append([j, x - ydown]) Qpot[i] = [x, Qpoti] return Qpot
def make_slab(unit_cell, num_layers, vacuum=0.0, d_fix=5., free_atoms=[], axis=-1): '''Makes a slab for GSF calculation, with <num_layers> atomic layers, a vacuum buffer of thickness <vacuum is added to the top, and <constraints> (a list of functions testing specific constraints, such as proximity to the buffer, atom type, etc.) are applied. ### NEEDS TO BE GENERALIZED ### ''' # set up slab, without the vacuum layer new_dimensions = np.ones(3) new_dimensions[axis] = num_layers slab = cry.superConstructor(unit_cell, dims=new_dimensions) # total height of cell, including vacuum layer old_height = norm(slab.getVector(axis)) new_height = old_height + vacuum # test to see if there is a vacuum layer, in which case a proximity # constraint will be applied if vacuum > 1e-10: print('Non-zero vacuum buffer. Proximity constraint to be used ' + 'with d_fix = {:.1f}.'.format(d_fix)) use_vacuum = True else: print('3D-periodic boundary conditions. Proximity constraints' + ' will not be applied.') use_vacuum = False # apply constraints for atom in slab.getAtoms(): # fix atom if it is within d_fix of the slab-vacuum interface, provided # that vacuum thickness is non-zero -> notify user if this constraint is # set if use_vacuum: near_interface = proximity_constraint(atom, old_height, d_fix, axis) else: near_interface = False if not near_interface: if atom.getSpecies() in free_atoms or free_atoms == 'all': # atom allowed to relax freely pass else: # relax normal to the slip plane atom.set_constraints(cry.ei(3, usetype=int)) # add vacuum to the top of the slab # begin by computing coordinates of all atoms in the vacuum-buffered slab if use_vacuum: for atom in slab: coords = atom.getCoordinates() new_length = coords[axis] * old_height / new_height new_coords = np.copy(coords) new_coords[axis] = new_length atom.setCoordinates(new_coords) coords_disp = atom.getDisplacedCoordinates() length_disp = coords_disp[axis] * old_height / new_height new_disp = np.copy(coords_disp) new_disp[axis] = length_disp atom.setDisplacedCoordinates(new_disp) # increase the height of the slab slab.setVector(slab.getVector(axis) * new_height / old_height, axis) return slab