def update_qm_region(atoms, dis_type='edge', cut=3.0, rr=10.0, qr=1): """ Routine for updating qm region of dislocation. Args: dis_type: Dislocation type can be edge or screw. rr: determines radius of quantum sphere. qr: is the number of quantum regions. """ core[:] = atoms.params['core'] fixed_mask = (np.sqrt((atoms.positions[:,0]-core[0])**2 + (atoms.positions[:,1]-core[1])**2) < rr) cl = atoms.select(mask=fixed_mask, orig_index=True) print 'Number of Atoms in Cluster', cl.n cl.set_cutoff(cut) cl.calc_connect() cl = Atoms(cl) x0 = Atoms('ref_slab.xyz') x0.set_cutoff(cut) x0.calc_connect() alpha = calc_nye_tensor(cl, x0, 3, 3, cl.n) cl.screw = alpha[2,2,:] cl.edge = alpha[2,0,:] if dis_type == 'screw': defect_pos = cl.screw elif dis_type == 'edge': defect_pos = cl.edge total_def = 0.0 c = np.array([0.,0.,0.]) mom = [3.0 for at in range(len(atoms))] atoms.set_initial_magnetic_moments(mom) for i in range(cl.n): defect_pos = defect_pos + cl.edge[i] c[1] = c[1] + cl.positions[i,0]*defect_pos[i] c[2] = c[2] + cl.pos[i,0]*defect_pos[i] c[0] = c[0]/total_def c[1] = c[1]/total_def c[2] = atoms.lattice[2,2]/2. core[:] = c.copy() old_qm_list = atoms.hybrid_vec.nonzero()[0] new_qm_list = update_hysteretic_qm_region(atoms, old_qm_list, core[:], qm_inner_radius, qm_outer_radius, update_marks=False) #Force Mixing Potential requires hybrid property: atoms.hybrid[:] = 0 atoms.hybrid[new_qm_list] = 1 #Distributed Force Mixing Properties: atoms.hybrid_vec[:] = 0 atoms.hybrid_vec[new_qm_list] = 1 atoms.hybrid_1[:] = atoms.hybrid_vec[:] atoms.params['core'] = core[:] return
def __init__(self, source, format=None, start=None, stop=None, step=None, cache_mem_limit=-1, rename=None, **kwargs): def file_exists(f): return f == "stdin" or os.path.exists(f) or len(glob.glob(f)) > 0 self.source = source self.format = format self._start = start self._stop = stop self._step = step self.cache_mem_limit = cache_mem_limit logging.debug('AtomsReader memory limit %r' % self.cache_mem_limit) self._source_len = None self._cache_dict = {} self._cache_list = [] self._cache_mem_usage = [] self.opened = False self.reader = source self.rename = rename if isinstance(self.reader, basestring): if '@' in self.reader: self.reader, frames = self.reader.split('@') frames = parse_slice(frames) if start is not None or stop is not None or step is not None: raise ValueError( 'Conflicting frame references start=%r stop=%r step=%r and @-sytnax %r' % (start, stop, step, frames)) if isinstance(frames, int): if frames >= 0: frames = slice(frames, frames + 1, +1) else: frames = slice(frames, frames - 1, -1) self._start, self._stop, self._step = frames.start, frames.stop, frames.step self.filename = self.reader self.opened = True if self.reader in AtomsReaders: if format is None: format = self.reader elif format != 'string': self.reader = os.path.expanduser(self.reader) glob_list = sorted(glob.glob(self.reader)) if (len(glob_list) == 0): raise IOError("input file '%s' not found" % self.reader) if len(glob_list) > 1: self.reader = glob_list else: self.reader = glob_list[0] filename, self.reader, new_format = infer_format( self.reader, format, AtomsReaders) if format is None: format = new_format # special cases if source is a list or tuple of filenames or Atoms objects is_filename_sequence = False is_list_of_atoms = False if isinstance(self.reader, list) or isinstance(self.reader, tuple): is_filename_sequence = True is_list_of_atoms = True for item in self.reader: if '@' in item: item = item[:item.index('@')] if not isinstance(item, basestring) or not file_exists(item): is_filename_sequence = False if not isinstance(item, Atoms): is_list_of_atoms = False if is_filename_sequence: self.reader = AtomsSequenceReader(self.reader, format=format, **kwargs) elif is_list_of_atoms: # dummy reader which copies from an existing list or tuple of Atoms objects self.reader = [at.copy() for at in self.reader] else: if format is None: format = self.reader.__class__ if format in AtomsReaders: self.reader = AtomsReaders[format](self.reader, **kwargs) # check if reader is still a string or list of strings - indicates missing files or unknown format if isinstance(self.reader, basestring): raise IOError("Cannot read Atoms from file %s" % self.reader) elif isinstance(self.reader, list): is_list_of_strings = True for item in self.reader: if not isinstance(item, basestring): is_list_of_strings = False break if is_list_of_strings: raise IOError("Cannot read Atoms from files %s" % self.reader) if isinstance(self.reader, AtomsReader): self.reader = AtomsReaderCopier(self.reader) if not hasattr(self.reader, '__iter__'): # call Atoms constructor - this has beneficial side effect of making a copy self.reader = [Atoms(self.reader)]
def CubeReader(f, property_name='charge', discard_repeat=True, format=None): def convert_line(line, *fmts): return (f(s) for f, s in zip(fmts, line.split())) if type(f) == type(''): f = open(f) opened = True # First two lines are comments comment1 = f.readline() comment2 = f.readline() # Now number of atoms and origin n_atom, origin_x, origin_y, origin_z = convert_line( f.readline(), int, float, float, float) origin = farray([origin_x, origin_y, origin_z]) * BOHR # Next three lines define number of voxels and shape of each element shape = [0, 0, 0] voxel = fzeros((3, 3)) for i in (1, 2, 3): shape[i - 1], voxel[1, i], voxel[2, i], voxel[3, i] = convert_line( f.readline(), int, float, float, float) at = Atoms(n=n_atom, lattice=voxel * BOHR * shape) at.add_property(property_name, 0.0) prop_array = getattr(at, property_name) # Now there's one line per atom for i in frange(at.n): at.z[i], prop_array[i], at.pos[1, i], at.pos[2, i], at.pos[3, i] = convert_line( f.readline(), int, float, float, float, float) at.pos[:, i] *= BOHR at.set_atoms(at.z) # Rest of file is volumetric data data = np.fromiter((float(x) for x in f.read().split()), float, count=-1) if data.size != shape[0] * shape[1] * shape[2]: raise IOError("Bad array length - expected shape %r, but got size %d" % (shape, data.size)) # Save volumetric data in at.data data = farray(data.reshape(shape)) # Discard periodic repeats? if discard_repeat: at.data = data[:-1, :-1, :-1] shape = [s - 1 for s in shape] at.set_lattice(voxel * BOHR * shape, False) at.params['comment1'] = comment1 at.params['comment2'] = comment2 at.params['origin'] = origin at.params['shape'] = shape # save grids in at.grid_x, at.grid_y, at.grid_z if at.is_orthorhombic: at.grid_x, at.grid_y, at.grid_z = np.mgrid[origin[1]:origin[1] + at.lattice[1, 1]:shape[0] * 1j, origin[2]:origin[2] + at.lattice[2, 2]:shape[1] * 1j, origin[3]:origin[3] + at.lattice[3, 3]:shape[2] * 1j] if opened: f.close() yield at
del final[remove_index] for (i, ii) in enumerate(initial.arrays['ind']): if ii == remove_index_f: new_remove_index_f = i remove_index_f = new_remove_index_f final.positions[remove_index_f] = orig_pos final.set_calculator(model.calculator) print 'relaxing final config' final = relax_atoms(final, tol=tol, traj_file=None) #final = ase.io.read(os.path.join("final-relax", "castep.castep")) final.set_calculator(model.calculator) # make chain images = [Atoms(initial)] images += [Atoms(initial.copy()) for i in range(n_images)] images += [Atoms(final)] neb = NEB(images, k=0.1) neb.interpolate() for img in images: img.set_calculator(model.calculator) ase.io.write(sys.stdout, img, 'extxyz') # perturb intermediate images for img in images[1:-1]: img.rattle(0.05) # optimizer = FIRE(neb) optimizer = MDMin(neb, dt=0.05)
def CP2KDirectoryReader(run_dir, at_ref=None, proj='quip', calc_qm_charges=None, calc_virial=False, out_i=None, qm_vacuum=6.0, run_suffix='_extended', format=None): if at_ref is None: filepot_xyz = os.path.join(run_dir, 'filepot.xyz') if not os.path.exists(filepot_xyz): # try looking up one level filepot_xyz = os.path.join(run_dir, '../filepot.xyz') if os.path.exists(filepot_xyz): at_ref = Atoms(filepot_xyz) else: at_ref = Atoms(os.path.join(run_dir, 'cp2k_output.out'), format='cp2k_output') at = at_ref.copy() cp2k_output_filename, cp2k_output = read_text_file( os.path.join(run_dir, 'cp2k_output.out')) cp2k_params = CP2KInputHeader( os.path.join(run_dir, 'cp2k_input.inp.header')) at.params.update(cp2k_params) run_type = cp2k_run_type(cp2k_output=cp2k_output, cp2k_input_header=cp2k_params) try: cluster_mark = getattr(at, 'cluster_mark' + run_suffix) qm_list_a = ((cluster_mark != HYBRID_NO_MARK).nonzero()[0]).astype( np.int32) except AttributeError: qm_list_a = fzeros(0, dtype=np.int32) if calc_qm_charges is None: calc_qm_charges = '' try: cur_qmmm_qm_abc = [ float(cp2k_params['QMMM_ABC_X']), float(cp2k_params['QMMM_ABC_Y']), float(cp2k_params['QMMM_ABC_Z']) ] except KeyError: if 'QM_cell' + run_suffix in at.params: cur_qmmm_qm_abc = at.params['QM_cell' + run_suffix] else: cur_qmmm_qm_abc = qmmm_qm_abc(at, qm_list_a, qm_vacuum) quip_cp2k_at = Atoms(os.path.join(run_dir, 'quip_cp2k.xyz')) rev_sort_index_file = os.path.join(run_dir, '../quip_rev_sort_index') fields = [int(x) for x in open(rev_sort_index_file).read().split()] rev_sort_index = farray(fields, dtype=np.int32) #verbosity_push(PRINT_SILENT) cp2k_energy, cp2k_force = read_output(quip_cp2k_at, qm_list_a, cur_qmmm_qm_abc, run_dir, proj, calc_qm_charges, calc_virial, True, 3, at.n, out_i) #verbosity_pop() qm_list = None if os.path.exists(os.path.join(run_dir, 'cp2k_input.qmmm_qm_kind')): qm_kind_grep_cmd = "grep MM_INDEX %s/cp2k_input.qmmm_qm_kind | awk '{print $2}'" % run_dir qm_list = [int(i) for i in os.popen(qm_kind_grep_cmd).read().split()] if qm_list is not None: if run_type == 'QMMM': reordering_index = getattr(at, 'reordering_index', None) at.add_property('qm', False, overwrite=True) if reordering_index is not None: qm_list = reordering_index[qm_list] at.qm[qm_list] = True elif run_type == 'QS': at.add_property('qm_orig_index', 0, overwrite=True) for i, qm_at in fenumerate(qm_list): at.qm_orig_index[i] = sort_index[qm_at] at.add_property('force', cp2k_force, overwrite=True) at.params['energy'] = cp2k_energy yield at
def find_crack_tip_coordination(atoms, edge_tol=10.0, strip_height=30.0, nneightol=1.3): """ Return position of crack tip in `atoms`, based on atomic coordination. If `atoms` does not contain an `advance_map` property, then :func:`make_crack_advance_map` is called to generate the map. Parameters ---------- atoms : :class:`~.Atoms' object The Atoms object containing the crack slab. edge_tol : float Distance from edge of system within which to exclude undercoodinated atoms. strip_height : float Height of strip along centre of slab in which to look for the track. nneightol : float Nearest neighbour tolerance, as a fraction of sum of covalent radii of atomic species. Returns ------- crack_pos : array x, y, and z coordinates of the crack tip. Also set in ``CrackPos`` in ``atoms.info`` dictionary. tip_atoms : array Indices of atoms near the tip Also set in ``crack_tip`` property. """ old_tip_pos_y = 0 if 'CrackPos' in atoms.info: old_tip_pos_y = atoms.info['CrackPos'][1] # Make a copy of atoms as a quippy.Atoms instance, overwriting # positions with time-averages values if they are available, and # then calculate connectivity using nneightol tmp_atoms = Atoms(atoms) if 'avgpos' in tmp_atoms.arrays: tmp_atoms.set_positions(tmp_atoms.arrays['avgpos']) tmp_atoms.calc_connect() nn = tmp_atoms.n_neighb x = tmp_atoms.positions[:, 0] y = tmp_atoms.positions[:, 1] # find undercoordinated atoms in a central strip, and not too # close to the left or right edges left = tmp_atoms.positions[:, 0].min() right = tmp_atoms.positions[:, 0].max() uc = ((nn < 4) & (abs(y) < strip_height) & (x > left + edge_tol) & (x < right - edge_tol)) # position of furthest forward undercoordinated atom ABOVE old tip position x_above = x[uc & (y > old_tip_pos_y)].max() # position of furthest forward undercoordinated atom BELOW old tip position x_below = x[uc & (y < old_tip_pos_y)].max() # rightmost undercoordinated atoms, both above and below old tip rightmost_uc = uc & (((y > old_tip_pos_y) & (x_above == x)) | ((y <= old_tip_pos_y) & (x_below == x))) # we want the NEXT pair of atoms, so we use the saved mapping from # atom indices to the indices of atoms one unit cell to the right if 'advance_map' not in atoms.arrays: print('Generating crack advance map...') make_crack_advance_map(atoms) advance_map = atoms.arrays['advance_map'] tip_atoms = advance_map[rightmost_uc] tip_pos = tmp_atoms.positions[tip_atoms, :].mean(axis=0) # Also save results in Atoms (useful for visualisation) atoms.info['CrackPos'] = tip_pos atoms.set_array('crack_tip', np.array([False]*len(atoms))) crack_tip = atoms.arrays['crack_tip'] crack_tip[tip_atoms] = True return tip_pos
#!/usr/bin/env python import sys import os from math import pi import numpy as np from quippy.atoms import Atoms from quippy.structures import void_analysis from quippy.farray import fzeros infile = sys.argv[1] basename = os.path.splitext(infile)[0] a = Atoms(infile) grid_size = 0.25 min_void_size = 2.0 nx = int((a.pos[1,:].max() - a.pos[1,:].min())/grid_size) ny = int((a.pos[2,:].max() - a.pos[2,:].min())/grid_size) nz = int((a.pos[3,:].max() - a.pos[3,:].min())/grid_size) n = nx*ny*nz grid = fzeros((3, n)) radii = fzeros(n) void_analysis(a, grid_size, min_void_size, grid, radii) extent = (grid[:,-1] - grid[:,1])
# additional parameters for the QM/MM simulation: qm_init_args = 'TB DFTB' # Initialisation arguments for QM potential qm_inner_radius = 8.0 * units.Ang # Inner hysteretic radius for QM region qm_outer_radius = 10.0 * units.Ang # Inner hysteretic radius for QM region extrapolate_steps = 10 # Number of steps for predictor-corrector # interpolation and extrapolation # ******* End of parameters ************* set_fortran_indexing(False) # ********** Read input file ************ print 'Loading atoms from file %s' % input_file atoms = Atoms(input_file) orig_height = atoms.info['OrigHeight'] orig_crack_pos = atoms.info['CrackPos'].copy() # ***** Setup constraints ******* top = atoms.positions[:, 1].max() bottom = atoms.positions[:, 1].min() left = atoms.positions[:, 0].min() right = atoms.positions[:, 0].max() # fix atoms in the top and bottom rows fixed_mask = ((abs(atoms.positions[:, 1] - top) < 1.0) | (abs(atoms.positions[:, 1] - bottom) < 1.0)) fix_atoms = FixAtoms(mask=fixed_mask)
def VASP_POSCAR_Reader(poscar, species=None, format=None): """Read a configuration from a VASP POSCAR file. Following POSCAR, optionally also read a trajectory from an OUTCAR file.""" p = open(poscar, 'r') comment = p.readline().rstrip() l = p.readline().strip() lc_factor = float(l) l = p.readline().strip() a1 = np.real(l.split()) l = p.readline().strip() a2 = np.real(l.split()) l = p.readline().strip() a3 = np.real(l.split()) l = p.readline().strip() at_species = l.split() try: ns = [int(n) for n in at_species] no_species = True except: no_species = False have_species = True if (no_species): for i in range(len(ns)): if (species is not None): species_cli = species.split() at_species[i] = species_cli[i - 1] else: have_species = False at_species[i] = ("%d" % (i + 1)) else: l = p.readline().strip() ns = [int(n) for n in l.split()] l = p.readline().strip() if (re.compile("^\s*s", re.IGNORECASE).match(l)): dyn_type = l coord_type = p.readline().strip() else: coord_type = l n = 0 for i in range(len(ns)): n += ns[i] lat = fzeros((3, 3)) lat[:, 1] = a1[0:3] lat[:, 2] = a2[0:3] lat[:, 3] = a3[0:3] lat *= lc_factor at = Atoms(n=n, lattice=lat) if (len(comment) > 0): at.params['VASP_Comment'] = comment coord_direct = re.compile("^\s*d", re.IGNORECASE).match(coord_type) ii = 1 for ti in range(len(ns)): for i in range(ns[ti]): l = p.readline().strip() pos = np.array(l.split()[0:3], float) if (coord_direct): at.pos[:, ii] = np.dot(at.lattice[:, :], pos[:]) else: at.pos[:, ii] = pos[:] * lc_factor at.species[:, ii] = at_species[ti] ii += 1 if (have_species): at.set_zs() else: at.Z[:] = [int("".join(n)) for n in at.species[:]] yield at
def VASP_XDATCAR_Reader(xdatcar, species=None, format=None): p = open(xdatcar, 'r') def read_header(xdatcar): comment = p.readline().rstrip() if comment.find("Direct configuration") == 0: # is the beginning of some positions return (None, None, comment, None, None, None) l = p.readline().strip(); lc_factor=float(l) l = p.readline().strip(); a1 = np.real(l.split()) l = p.readline().strip(); a2 = np.real(l.split()) l = p.readline().strip(); a3 = np.real(l.split()) l = p.readline().strip(); at_species = l.split() try: ns = [ int(n) for n in at_species ] no_species_read = True except: no_species_read = False if species is not None: at_species = species.split() have_species = True else: if no_species_read: have_species = False for i in range(len(ns)): at_species[i] = ("%d" % (i+1)) else: have_species = True if not no_species_read: l = p.readline().strip(); ns = [ int(n) for n in l.split() ] n=0 for i in range(len(ns)): n += ns[i] lat = fzeros( (3,3) ) lat[:,1] = a1[0:3] lat[:,2] = a2[0:3] lat[:,3] = a3[0:3] lat *= lc_factor return (lat, n, comment, ns, at_species, have_species) (lat, n, comment, ns, at_species, have_species) = read_header(xdatcar) # end of header while True: try: (new_lat, new_n, new_comment, new_ns, new_at_species, new_have_species) = read_header(xdatcar) except: return if new_lat is None: # wasn't another header l = new_comment else: lat = new_lat # assume nothing else has changed l=p.readline().strip() if (re.compile("^\s*s", re.IGNORECASE).match(l)): dyn_type = l coord_type = p.readline().strip(); else: coord_type = l at = Atoms(n=n, lattice=lat) if (len(comment) > 0): at.params['VASP_Comment'] = comment coord_direct=re.compile("^\s*d", re.IGNORECASE).match(coord_type); ii = 1 for ti in range(len(ns)): for i in range(ns[ti]): l = p.readline().strip(); pos = np.array(l.split()[0:3], float); if (coord_direct): at.pos[:,ii] = np.dot(at.lattice[:,:],pos[:]) else: at.pos[:,ii] = pos[:]*lc_factor at.species[:,ii] = at_species[ti] ii += 1 if (have_species): at.set_zs() else: at.Z[:] = [ int("".join(n)) for n in at.species[:] ] yield at
def VASP_POSCAR_Reader(outcar, species=None, format=None): """Read a configuration from a VASP OUTCAR file.""" if (outcar == 'stdin' or outcar == '-'): p = sys.stdin else: p = open(outcar, 'r') re_comment = re.compile("\s*POSCAR:\s*(.+)") re_potcar = re.compile("\s*POTCAR:\s*\S+\s+(\S+)") re_n_atoms = re.compile("\s*ions per type =\s*((?:\d+\s*)*)") energy_i = -1 at_i = -1 lat_i = -1 elements = [] n_at = -1 at_cur = None for lr in p: l = lr.rstrip() if (n_at <= 0): # parse header type things m = re_comment.match(l) if (m is not None): VASP_Comment = m.group(1) # print "got VASP_Comment '%s'" % VASP_Comment m = re_potcar.match(l) if (m is not None): elements.append(m.group(1)) m = re_n_atoms.match(l) if (m is not None): # print "got ions per type, groups are:" # print m.groups() lat = fzeros((3, 3)) n_types = [int(f) for f in m.group(1).split()] n_at = sum(n_types) at = Atoms(n=n_at, latttice=lat) i_at = 0 for type_i in range(len(n_types)): for j in range(n_types[type_i]): i_at += 1 # print "set species of atom %d to '%s'" % (i_at, elements[type_i]) at.species[i_at] = elements[type_i] at.set_zs() else: # parse per-config lattice/pos/force if (l.find("direct lattice vectors") >= 0): # get ready to read lattice vectors at_cur = at.copy() lat_cur = fzeros((3, 3)) lat_i = 1 elif (lat_i >= 1 and lat_i <= 3): # read lattice vectors lat_cur[:, lat_i] = [ float(r) for r in l.replace("-", " -").split()[0:3] ] lat_i += 1 elif (l.find("TOTAL-FORCE (eV/Angst)") >= 0): # get ready to read atomic positions and forces if (not hasattr(at_cur, "force")): at_cur.add_property("force", 0.0, n_cols=3) at_i = 1 p.next() elif (at_i >= 1 and at_i <= at_cur.n): # read atomic positions and forces pos_force = [ float(r) for r in l.replace("-", " -").split()[0:6] ] at_cur.pos[:, at_i] = pos_force[0:3] at_cur.force[:, at_i] = pos_force[3:6] at_i += 1 elif (l.find("free energy") >= 0): # get ready to read energy at_cur.params['Energy'] = float(l.split()[4]) energy_i = 1 p.next() elif (energy_i == 1): # read energy # print "energy(sigma->0) line" # print l.split() at_cur.params['Energy_sigma_to_zero'] = float(l.split()[6]) energy_i += 1 yield at_cur if (at_cur is not None and at_i == at_cur.n): # at end of configuration, set lattice at_cur.set_lattice(lat_cur, False) __all__ = ['VASP_POSCAR_Reader', 'VASP_OUTCAR_Reader', 'VASPWriter']
def __iter__(self): if type(self.filename) == type(''): f = open(self.filename) opened = True # First eight lines are header header = [f.readline() for i in range(8)] # Rest of file is data array data = np.loadtxt(f) lattice = fzeros((3, 3)) for i in [1, 2, 3]: lattice[:, i] = [float(x) for x in header[i + 1].split()[1:]] if self.vacuum is not None: lattice += np.diag(self.vacuum) at = Atoms(n=len(data), lattice=lattice) at.pos[...] = data[:, 3:6].T at.set_atoms(self.z) at.add_property('tag', data[:, 1].astype(int)) at.add_property('mass', data[:, 2]) at.add_property('velo', data[:, 6:9].T) at.add_property('epot', data[:, 9]) if self.fix_tags is not None: at.add_property('move_mask', 1) for tag in self.fix_tags: at.move_mask[at.tag == tag] = 0 if opened: f.close() yield at
clients.append(client) print 'All calculations queued, waiting for results.' t_clientstart = time.time() # wait for input queues to empty for input_q in input_qs: input_q.join() t_clientrun = time.time() print 'Input queues drained. Shutting down clients.' # stop the clients by sending them a calculation with zero atoms dummy_at = Atoms(n=0, lattice=np.eye(3)) dummy_data = pack_atoms_to_reftraj_str(dummy_at, 0) for input_q in input_qs: input_q.put(dummy_data) # wait for them all to shutdown for client in clients: client.wait() print 'Clients terminated.' for stdout in stdouts: stdout.flush() stdout.close() print 'Client logs flushed.'
if MOLPRO_TEMPLATE[-4:] != '.xml': # Read template input file try: datafile = molpro.MolproDatafile(MOLPRO_TEMPLATE) #datafile.write() except IOError: die("Can't open input file %s" % MOLPRO_TEMPLATE) except ValueError, message: die(str(message)) # need to add XML handling here # Read extended XYZ input file containing cluster cluster = Atoms(xyzfile) # remove old output file, if it's there if os.path.exists(outfile): os.remove(outfile) path = WORKING_DIR+'/'+stem # Make working directory if necessary if not os.path.isdir(path): os.mkdir(path) os.chdir(path) if not BATCH_READ: # Load up old cluster, if it's there
def __init__(self, atoms, timestep, trajectory, trajectoryinterval=10, initialtemperature=None, logfile='-', loginterval=1, loglabel='D'): # we will do the calculation in place, to minimise number of copies, # unless atoms is not a quippy Atoms if not isinstance(atoms, Atoms): warnings.warn( 'Dynamics atoms is not quippy.Atoms instance, copy forced!') atoms = Atoms(atoms) self.atoms = atoms if self.atoms.has('masses'): if self.atoms.has_property('mass'): if np.max( np.abs(self.atoms.mass / MASSCONVERT - self.atoms.get_masses())) > 1e-3: raise RuntimeError( 'Dynamics confused as atoms has inconsistent "mass" and "masses" arrays' ) else: self.atoms.add_property('mass', self.atoms.get_masses() * MASSCONVERT) else: if self.atoms.has_property('mass'): self.atoms.set_masses(self.atoms.mass / MASSCONVERT) else: self.atoms.set_masses('defaults') if self.atoms.has('momenta'): if self.atoms.has_property('velo'): if np.max( np.abs(self.atoms.velo * sqrt(MASSCONVERT) - self.atoms.get_velocities().T)) > 1e-3: raise RuntimeError( 'Dynamics confused as atoms has inconsistent "velo" and "momenta" arrays' ) else: self.atoms.add_property('velo', (self.atoms.get_velocities() / sqrt(MASSCONVERT)).T) else: if self.atoms.has_property('velo'): self.atoms.set_velocities(self.atoms.velo.T * sqrt(MASSCONVERT)) else: # start with zero momenta self.atoms.set_momenta(np.zeros_like(self.atoms.positions)) self.atoms.add_property('velo', 0., n_cols=3) self._ds = DynamicalSystem(self.atoms) if initialtemperature is not None: if np.max(np.abs(self._ds.atoms.velo)) > 1e-3: msg = ('initialtemperature given but Atoms already ' + 'has non-zero velocities!') raise RuntimeError(msg) self._ds.rescale_velo(initialtemperature) # now self._ds.atoms is either a Fortran shallowcopy of atoms, # or a copy if input atoms was not an instance of quippy.Atoms if 'time' in atoms.info: self.set_time(atoms.info['time']) # from ASE units to fs self.observers = [] self.set_timestep(timestep) if trajectory is not None: if isinstance(trajectory, basestring): trajectory = AtomsWriter(trajectory) self.attach(trajectory, trajectoryinterval, self._ds.atoms) self.loglabel = loglabel if logfile is not None: if isinstance(logfile, basestring): logfile = InOutput(logfile, OUTPUT) self.attach(self.print_status, loginterval, logfile) self._calc_virial = False self._virial = np.zeros((3, 3))
def PosCelReader(basename=None, pos='pos.in', cel='cel.in', force='force.in', energy='energy.in', stress='stress.in', species_map={ 'O': 1, 'Si': 2 }, cel_angstrom=False, pos_angstrom=False, rydberg=True, format=None): if basename is not None: basename = os.path.splitext(basename)[0] pos = '%s.pos' % basename cel = '%s.cel' % basename energy = '%s.ene' % basename stress = '%s.str' % basename force = '%s.for' % basename doenergy = os.path.exists(energy) doforce = os.path.exists(force) dostress = os.path.exists(stress) if isinstance(pos, str): pos = open(pos) if isinstance(cel, str): cel = open(cel) if doenergy and isinstance(energy, str): energy = open(energy) if doforce and isinstance(force, str): force = open(force) if dostress and isinstance(stress, str): stress = open(stress) pos = iter(pos) cel = iter(cel) if doenergy: energy = iter(energy) if doforce: force = iter(force) if dostress: stress = iter(stress) pos.next() # throw away blank line at start if doforce: force.next() rev_species_map = dict(zip(species_map.values(), species_map.keys())) while True: poslines = list( itertools.takewhile( lambda L: L.strip() != '' and not L.strip().startswith('STEP'), pos)) if poslines == []: break cellines = list(itertools.islice(cel, 4)) #lattice = farray([ [float(x) for x in L.split()] for L in cellines[1:4] ]).T lattice = fzeros((3, 3)) for i in (1, 2, 3): lattice[:, i] = [float(x) for x in cellines[i].split()] if not cel_angstrom: lattice *= BOHR at = Atoms(n=len(poslines), lattice=lattice) at.pos[:] = farray([[float(x) for x in L.split()[0:3]] for L in poslines]).T if not pos_angstrom: at.pos[:] *= BOHR species = [rev_species_map[int(L.split()[3])] for L in poslines] elements = [ not el.isdigit() and atomic_number(el) or el for el in species ] at.set_atoms(elements) if doenergy: at.params['energy'] = float(energy.next().split()[0]) if rydberg: at.params['energy'] *= RYDBERG if dostress: stress_lines = list(itertools.islice(stress, 4)) virial = farray([[float(x) for x in L.split()] for L in stress_lines[1:4]]) virial *= at.cell_volume() / (10.0 * GPA) at.params['virial'] = virial if doforce: at.add_property('force', 0.0, n_cols=3) force_lines = list( itertools.takewhile(lambda L: L.strip() != '', force)) if len(force_lines) != at.n: raise ValueError("len(force_lines) (%d) != at.n (%d)" % (len(force_lines), at.n)) at.force[:] = farray([[float(x) for x in L.split()[0:3]] for L in force_lines]).T if rydberg: at.force[:] *= RYDBERG / BOHR yield at
strain_rate = 1e-5*(1/units.fs) # Strain rate traj_file = 'traj.nc' # Trajectory output file (NetCDF format) traj_interval = 10 # Number of time steps between # writing output frames param_file = 'params.xml' # Filename of XML file containing # potential parameters mm_init_args = 'IP SW' # Initialisation arguments for # classical potential # ******* End of parameters ************* # ********** Read input file ************ print 'Loading atoms from file %s' % input_file atoms = Atoms(input_file, fortran_indexing=False) orig_height = atoms.info['OrigHeight'] orig_crack_pos = atoms.info['CrackPos'].copy() # ***** Setup constraints ******* top = atoms.positions[:, 1].max() bottom = atoms.positions[:, 1].min() left = atoms.positions[:, 0].min() right = atoms.positions[:, 0].max() # fix atoms in the top and bottom rows fixed_mask = ((abs(atoms.positions[:, 1] - top) < 1.0) | (abs(atoms.positions[:, 1] - bottom) < 1.0)) fix_atoms = FixAtoms(mask=fixed_mask)
param_file = 'params.xml' # XML file containing interatomic potential parameters mm_init_args = 'IP SW' # Initialisation arguments for the classical potential output_file = 'crack.xyz' # File to which structure will be written # ******* End of parameters ************* set_fortran_indexing(False) # ********** Build unit cell ************ # 8-atom diamond cubic unit cell for silicon, with guess at lattice # constant of 5.44 A si_bulk = bulk('Si', 'diamond', a=5.44, cubic=True) si_bulk = Atoms(si_bulk) # ********** Setup potential ************ # Stillinger-Weber (SW) classical interatomic potential, from QUIP mm_pot = Potential(mm_init_args, param_filename=param_file, cutoff_skin=0.) # ***** Find eqm. lattice constant ****** # find the equilibrium lattice constant by minimising atoms wrt virial # tensor given by SW pot (possibly replace this with parabola fit in # another script and hardcoded a0 here) si_bulk.set_calculator(mm_pot) print('Minimising bulk unit cell...') minim = Minim(si_bulk, relax_positions=True, relax_cell=True)
def read_xml_output(xmlfile,energy_from=None, extract_forces=False, extract_dipole=False, datafile=None, cluster=None): #parse an xml output file and return cluster with updated info # datafile tells which energies, forces to look for, cluster Atoms object which gets returned, this is echoed in the xml file so can be left out # If extract_forces is not given and the FORCE keyword is found in datafile, the default is to set extract_forces=True log = logging.getLogger('molpro_driver') if datafile is None: datafile=MolproDatafile(xml=xmlfile) if 'FORCE' in datafile: extract_forces=True energy_names = OrderedDict() energy_names['CCSD(T)-F12'] = ["total energy"] energy_names['CCSD(T)'] = ["total energy"] energy_names['MP2'] = ["total energy"] energy_names['DF-MP2'] = ["total energy"] energy_names['DF-RMP2'] = ["energy"] energy_names['RKS'] = ["Energy"] energy_names['RHF'] = ["Energy"] energy_names['DF-RHF'] = ["Energy"] energy_names['HF'] = ["Energy"] energy_names['DF-HF'] = ["Energy"] #etc gradient_names = OrderedDict() gradient_names['CCSD(T)'] =[""] gradient_names['RKS'] =['RKS GRADIENT'] gradient_names['MP2'] =['MP2 GRADIENT'] all_methods=OrderedDict() all_methods['HF']=["RHF"] all_methods['DF-HF']=["RHF"] all_methods['RHF']=["RHF"] all_methods['DF-RHF']=["RHF"] all_methods['MP2']=["MP2"] all_methods['DF-MP2']=["MP2"] all_methods['DF-RMP2']=["DF-RMP2"] all_methods['RKS']=["RKS"] all_methods['CCSD(T)-F12']=["CCSD(T)-F12a","CCSD(T)-F12b"] all_methods['CCSD(T)']=["CCSD(T)"] if energy_from is None: log.critical("don't know which energy to extract, use keyword energy_from with options "+str([all_methods[k] for k in iter(all_methods)]).replace('[','').replace(']','')) #loop through datafile to look for methods. calcs=[] #holds the keys for getting correct method, energy_name, gradient_name data_keys_upper = [key.upper() for key in datafile._keys] for key in all_methods._keys: if key in data_keys_upper: calcs.append(key) dom = minidom.parse(xmlfile) elements=[] position_matrix=[] cml = dom.documentElement.getElementsByTagName('cml:atomArray') for l in cml[0].childNodes: if l.nodeType== 1: element=l.attributes['elementType'].value.encode('ascii','ignore') elements.append(atomic_number(element)) posx = l.attributes['x3'].value.encode('ascii','ignore') posy = l.attributes['y3'].value.encode('ascii','ignore') posz = l.attributes['z3'].value.encode('ascii','ignore') position_matrix.append([float(posx),float(posy),float(posz)]) if cluster is None: cluster = Atoms(n=len(elements)) cluster.set_atoms(elements) position_matrix=farray(position_matrix).T if not 'ANGSTROM' in datafile._keys and not 'angstrom' in datafile._keys: position_matrix = position_matrix * (1.0/0.529177249) cluster.pos[:,:]=position_matrix #note this leaves the lattice undefined #now look for each of these energies in xml file energy_found=False props = dom.documentElement.getElementsByTagName('property') for prop in props: prop_name = prop.attributes['name'].value.encode('ascii','ignore') prop_method = prop.attributes['method'].value.encode('ascii','ignore') for calc in calcs: if prop_name in energy_names[calc] and prop_method in all_methods[calc]: energy_param_name="_".join([prop_method,prop_name]) energy_param_name=energy_param_name.replace(" ","_") #log.info("found "+energy_param_name) # dated routines for finding monomer pairs, triplets in Topology module energy_param=prop.attributes['value'].value.encode('ascii','ignore') my_energy=energy_param_name i_en=1 while my_energy in cluster.params.iterkeys(): i_en+=1 my_energy='_'.join([energy_param_name,str(i_en)]) cluster.params[my_energy] = float(energy_param) * HARTREE if prop_method == energy_from: cluster.params['Energy']=float(energy_param) * HARTREE energy_found=True elif extract_dipole and prop_name=='Dipole moment': dipole_param_name="_".join([prop_method,prop_name]) dipole_param_name=dipole_param_name.replace(" ","_") log.info("found dipole moment: "+dipole_param_name) dipole_param=prop.attributes['value'].value.encode('ascii','ignore') cluster.params[dipole_param_name]=dipole_param if not energy_found: log.critical("couldn't find energy from "+energy_from+" prop method : "+prop_method) # read gradients if requested if extract_forces: if not cluster.has_property('force'): cluster.add_property('force', 0.0, n_cols=3) grads = dom.documentElement.getElementsByTagName('gradient') force_matrix = grads[0].childNodes[0].data.split('\n') force_matrix = [str(i).split() for i in force_matrix] for i in force_matrix: try: force_matrix.remove([]) except ValueError: break force_matrix = [[(-1.0 * HARTREE / BOHR) * float(j) for j in i] for i in force_matrix] cluster.force[:] =farray(force_matrix).T if len(grads) != 1: for k in range(1,len(grads)): my_force='force%s'%str(k+1) force_matrix = grads[k].childNodes[0].data.split('\n') force_matrix = [str(i).split() for i in force_matrix] for i in force_matrix: try: force_matrix.remove([]) except ValueError: break force_matrix = [[(-1.0 * HARTREE / BOHR) * float(j) for j in i] for i in force_matrix] cluster.add_property(my_force,farray(force_matrix).T) return cluster
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) # we will do the calculation in place, to minimise number of copies, # unless atoms is not a quippy Atoms if isinstance(atoms, Atoms): self.quippy_atoms = weakref.proxy(atoms) else: potlog.debug( 'Potential atoms is not quippy.Atoms instance, copy forced!') self.quippy_atoms = Atoms(atoms) initial_arrays = self.quippy_atoms.arrays.keys() initial_info = self.quippy_atoms.info.keys() if properties is None: properties = ['energy', 'forces', 'stress'] # Add any default properties properties = set(self.get_default_properties() + properties) if len(properties) == 0: raise RuntimeError('Nothing to calculate') if not self.calculation_required(atoms, properties): return args_map = { 'energy': { 'energy': None }, 'energies': { 'local_energy': None }, 'forces': { 'force': None }, 'stress': { 'virial': None }, 'numeric_forces': { 'force': 'numeric_force', 'force_using_fd': True, 'force_fd_delta': 1.0e-5 }, 'stresses': { 'local_virial': None }, 'elastic_constants': {}, 'unrelaxed_elastic_constants': {} } # list of properties that require a call to Potential.calc() calc_properties = [ 'energy', 'energies', 'forces', 'numeric_forces', 'stress', 'stresses' ] # list of other properties we know how to calculate other_properties = ['elastic_constants', 'unrelaxed_elastic_constants'] calc_args = {} calc_required = False for property in properties: if property in calc_properties: calc_required = True calc_args.update(args_map[property]) elif property not in other_properties: raise RuntimeError( "Don't know how to calculate property '%s'" % property) if calc_required: self.calc(self.quippy_atoms, args_str=dict_to_args_str(calc_args)) if 'energy' in properties: self.results['energy'] = float(self.quippy_atoms.energy) if 'energies' in properties: self.results['energies'] = self.quippy_atoms.local_energy.copy( ).view(np.ndarray) if 'forces' in properties: self.results['forces'] = self.quippy_atoms.force.copy().view( np.ndarray).T if 'numeric_forces' in properties: self.results[ 'numeric_forces'] = self.quippy_atoms.numeric_force.copy( ).view(np.ndarray).T if 'stress' in properties: stress = -self.quippy_atoms.virial.copy().view( np.ndarray) / self.quippy_atoms.get_volume() # convert to 6-element array in Voigt order self.results['stress'] = np.array([ stress[0, 0], stress[1, 1], stress[2, 2], stress[1, 2], stress[0, 2], stress[0, 1] ]) if 'stresses' in properties: lv = np.array(self.quippy_atoms.local_virial) # make a copy vol_per_atom = self.get( 'vol_per_atom', self.quippy_atoms.get_volume() / len(atoms)) if isinstance(vol_per_atom, basestring): vol_per_atom = self.quippy_atoms.arrays[vol_per_atom] self.results['stresses'] = -lv.T.reshape( (len(atoms), 3, 3), order='F') / vol_per_atom if 'elastic_constants' in properties: cij_dx = self.get('cij_dx', 1e-2) cij = fzeros((6, 6)) self.calc_elastic_constants(self.quippy_atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c=cij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): cij = cij.view(np.ndarray) self.results['elastic_constants'] = cij if 'unrelaxed_elastic_constants' in properties: cij_dx = self.get('cij_dx', 1e-2) c0ij = fzeros((6, 6)) self.calc_elastic_constants(self.quippy_atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c0=c0ij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): c0ij = c0ij.view(np.ndarray) self.results['unrelaxed_elastic_constants'] = c0ij # copy back any additional output data to results dictionary skip_keys = ['energy', 'force', 'virial', 'numeric_force'] for key in self.quippy_atoms.arrays.keys(): if key not in initial_arrays and key not in skip_keys: self.results[key] = self.quippy_atoms.arrays[key].copy() for key in self.quippy_atoms.info.keys(): if key not in initial_info and key not in skip_keys: if isinstance(self.quippy_atoms.info[key], np.ndarray): self.results[key] = self.quippy_atoms.info[key].copy() else: self.results[key] = self.quippy_atoms.info[key]
def crack_strain_energy_release_rate(at, bulk=None, f_min=.8, f_max=.9, stem=None, avg_pos=False): """ Compute strain energy release rate G from elastic potential energy in a strip """ print 'Analytical effective elastic modulus E\' = ', at.YoungsModulus/(1-at.PoissonRatio_yx**2), 'GPa' print 'Analytical energy release rate G = ', crack_measure_g(at, at.YoungsModulus, at.PoissonRatio_yx, at.OrigHeight), 'J/m^2' if bulk is None: if stem is None: raise ValueError('Either "bulk" or "stem" must be present') bulk = Atoms(stem+'_bulk.xyz') if not hasattr(at, 'local_energy') or not hasattr(bulk, 'energy'): if stem is None: raise ValueError('local_energy property not found in Atoms and "stem" is missing') xmlfile = stem+'.xml' params = CrackParams(xmlfile) pot = Potential(params.classical_args, param_filename=stem+'.xml') pot.print_() if not hasattr(at, 'local_energy'): if avg_pos: tmp_pos = at.pos.copy() at.pos[...] = at.avgpos at.set_cutoff(pot.cutoff()+1.) at.calc_connect() pot.calc(at, args_str="local_energy") if avg_pos: at.pos[...] = tmp_pos if not hasattr(bulk, 'energy'): bulk.set_cutoff(pot.cutoff()+1.) bulk.calc_connect() pot.calc(bulk, args_str='energy') h = at.pos[2,:].max() - at.pos[2,:].min() h0 = at.OrigHeight strain = (h - h0)/h0 print 'Applied strain', strain x_min = f_min*at.OrigWidth - at.OrigWidth/2. x_max = f_max*at.OrigWidth - at.OrigWidth/2. strip = np.logical_and(at.move_mask == 1, np.logical_and(at.pos[1,:] > x_min, at.pos[1,:] < x_max)) at.add_property('strip', strip, overwrite=True) strip_depth = at.lattice[3,3] strip_width = at.pos[1,strip].max() - at.pos[1,strip].min() strip_height = at.pos[2,strip].max() - at.pos[2,strip].min() strip_volume = strip_width*strip_height*strip_depth print 'Strip contains', strip.sum(), 'atoms', 'width', strip_width, 'height', strip_height, 'volume', strip_volume strain_energy_density = (at.local_energy[strip].sum() - bulk.energy/bulk.n*strip.sum())/strip_volume print 'Strain energy density in strip', strain_energy_density, 'eV/A**3' E_effective = 2*strain_energy_density/strain**2*GPA print 'Effective elastic modulus E =', E_effective, 'GPa' G_effective = strain_energy_density*strip_height*J_PER_M2 print 'Effective energy release rate G =', G_effective, 'J/m^2' return G_effective
class Potential(_potential.Potential, Calculator): __doc__ = update_doc_string( _potential.Potential.__doc__, r""" The :class:`Potential` class also implements the ASE :class:`ase.calculators.interface.Calculator` interface via the the :meth:`get_forces`, :meth:`get_stress`, :meth:`get_stresses`, :meth:`get_potential_energy`, :meth:`get_potential_energies` methods. For example:: atoms = diamond(5.44, 14) atoms.rattle(0.01) atoms.set_calculator(pot) forces = atoms.get_forces() print forces Note that the ASE force array is the transpose of the QUIP force array, so has shape (len(atoms), 3) rather than (3, len(atoms)). The optional arguments `pot1`, `pot2` and `bulk_scale` are used by ``Sum`` and ``ForceMixing`` potentials (see also wrapper class :class:`ForceMixingPotential`) An :class:`quippy.mpi_context.MPI_context` object can be passed as the `mpi_obj` argument to restrict the parallelisation of this potential to a subset of the The `callback` argument is used to implement the calculation of the :class:`Potential` in a Python function: see :meth:`set_callback` for an example. In addition to the builtin QUIP potentials, it is possible to use any ASE calculator as a QUIP potential by passing it as the `calculator` argument to the :class:`Potential` constructor, e.g.:: from ase.calculators.morse import MorsePotential pot = Potential(calculator=MorsePotential) `atoms` if given, is used to set the calculator associated with `atoms` to the new :class:`Potential` instance, by calling :meth:'.Atoms.set_calculator`. .. note:: QUIP potentials do not compute stress and per-atom stresses directly, but rather the virial tensor which has units of stress :math:`\times` volume, i.e. energy. If the total stress is requested, it is computed by dividing the virial by the atomic volume, obtained by calling :meth:`.Atoms.get_volume`. If per-atom stresses are requested, a per-atom volume is needed. By default this is taken to be the total volume divided by the number of atoms. In some cases, e.g. for systems containing large amounts of vacuum, this is not reasonable. The ``vol_per_atom`` calc_arg can be used either to give a single per-atom volume, or the name of an array in :attr:`.Atoms.arrays` containing volumes for each atom. """, signature= 'Potential(init_args[, pot1, pot2, param_str, param_filename, bulk_scale, mpi_obj, callback, calculator, atoms, calculation_always_required])' ) callback_map = {} implemented_properties = [ 'energy', 'energies', 'forces', 'stress', 'stresses', 'numeric_forces', 'elastic_constants', 'unrelaxed_elastic_constants' ] def __init__(self, init_args=None, pot1=None, pot2=None, param_str=None, param_filename=None, bulk_scale=None, mpi_obj=None, callback=None, calculator=None, atoms=None, calculation_always_required=False, fpointer=None, finalise=True, error=None, **kwargs): self._calc_args = {} self._default_properties = [] self.calculation_always_required = calculation_always_required Calculator.__init__(self, atoms=atoms) if callback is not None or calculator is not None: if init_args is None: init_args = 'callbackpot' param_dirname = None if param_filename is not None: param_str = open(param_filename).read() param_dirname = path.dirname(param_filename) or None if init_args is None and param_str is None: raise ValueError('Need one of init_args,param_str,param_filename') if init_args is not None: if init_args.lower().startswith('callbackpot'): if not 'label' in init_args: init_args = init_args + ' label=%d' % id(self) else: # if param_str missing, try to find default set of QUIP params, # falling back on a do-nothing parameter string. if param_str is None and pot1 is None and pot2 is None: try: param_str = quip_xml_parameters(init_args) except IOError: param_str = r'<params></params>' if kwargs != {}: if init_args is not None: init_args = init_args + ' ' + dict_to_args_str(kwargs) else: init_args = dict_to_args_str(kwargs) # Change to the xml directory to initialise, so that extra files # like sparseX can be found. old_dir = os.getcwd() try: if param_dirname is not None: os.chdir(param_dirname) _potential.Potential.__init__(self, init_args, pot1=pot1, pot2=pot2, param_str=param_str, bulk_scale=bulk_scale, mpi_obj=mpi_obj, fpointer=fpointer, finalise=finalise, error=error) finally: os.chdir(old_dir) if init_args is not None and init_args.lower().startswith( 'callbackpot'): _potential.Potential.set_callback(self, Potential.callback) if callback is not None: self.set_callback(callback) if calculator is not None: self.set_callback(calculator_callback_factory(calculator)) if atoms is not None: atoms.set_calculator(self) self.name = init_args __init__.__doc__ = _potential.Potential.__init__.__doc__ def calc(self, at, energy=None, force=None, virial=None, local_energy=None, local_virial=None, args_str=None, error=None, **kwargs): if not isinstance(args_str, basestring): args_str = dict_to_args_str(args_str) kw_args_str = dict_to_args_str(kwargs) args_str = ' '.join((self.get_calc_args_str(), kw_args_str, args_str)) if isinstance(energy, basestring): args_str = args_str + ' energy=%s' % energy energy = None if isinstance(energy, bool) and energy: args_str = args_str + ' energy' energy = None if isinstance(force, basestring): args_str = args_str + ' force=%s' % force force = None if isinstance(force, bool) and force: args_str = args_str + ' force' force = None if isinstance(virial, basestring): args_str = args_str + ' virial=%s' % virial virial = None if isinstance(virial, bool) and virial: args_str = args_str + ' virial' virial = None if isinstance(local_energy, basestring): args_str = args_str + ' local_energy=%s' % local_energy local_energy = None if isinstance(local_energy, bool) and local_energy: args_str = args_str + ' local_energy' local_energy = None if isinstance(local_virial, basestring): args_str = args_str + ' local_virial=%s' % local_virial local_virial = None if isinstance(local_virial, bool) and local_virial: args_str = args_str + ' local_virial' local_virial = None potlog.debug( 'Potential invoking calc() on n=%d atoms with args_str "%s"' % (len(at), args_str)) _potential.Potential.calc(self, at, energy, force, virial, local_energy, local_virial, args_str, error) calc.__doc__ = update_doc_string( _potential.Potential.calc.__doc__, """In Python, this method is overloaded to set the final args_str to :meth:`get_calc_args_str`, followed by any keyword arguments, followed by an explicit `args_str` argument if present. This ordering ensures arguments explicitly passed to :meth:`calc` will override any default arguments.""") @staticmethod def callback(at_ptr): from quippy import Atoms at = Atoms(fpointer=at_ptr, finalise=False) if 'label' not in at.params or at.params[ 'label'] not in Potential.callback_map: raise ValueError('Unknown Callback label %s' % at.params['label']) Potential.callback_map[at.params['label']](at) def set_callback(self, callback): """ For a :class:`Potential` of type `CallbackPot`, this method is used to set the callback function. `callback` should be a Python function (or other callable, such as a bound method or class instance) which takes a single argument, of type :class:`~quippy.atoms.Atoms`. Information about which properties should be computed can be obtained from the `calc_energy`, `calc_local_e`, `calc_force`, and `calc_virial` keys in `at.params`. Results should be returned either as `at.params` entries (for energy and virial) or by adding new atomic properties (for forces and local energy). Here's an example implementation of a simple callback:: def example_callback(at): if at.calc_energy: at.params['energy'] = ... if at.calc_force: at.add_property('force', 0.0, n_cols=3) at.force[:,:] = ... p = Potential('CallbackPot') p.set_callback(example_callback) p.calc(at, energy=True) print at.energy ... """ Potential.callback_map[str(id(self))] = callback def check_state(self, atoms, tol=1e-15): if self.calculation_always_required: return all_changes return Calculator.check_state(self, atoms, tol) def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) # we will do the calculation in place, to minimise number of copies, # unless atoms is not a quippy Atoms if isinstance(atoms, Atoms): self.quippy_atoms = weakref.proxy(atoms) else: potlog.debug( 'Potential atoms is not quippy.Atoms instance, copy forced!') self.quippy_atoms = Atoms(atoms) initial_arrays = self.quippy_atoms.arrays.keys() initial_info = self.quippy_atoms.info.keys() if properties is None: properties = ['energy', 'forces', 'stress'] # Add any default properties properties = set(self.get_default_properties() + properties) if len(properties) == 0: raise RuntimeError('Nothing to calculate') if not self.calculation_required(atoms, properties): return args_map = { 'energy': { 'energy': None }, 'energies': { 'local_energy': None }, 'forces': { 'force': None }, 'stress': { 'virial': None }, 'numeric_forces': { 'force': 'numeric_force', 'force_using_fd': True, 'force_fd_delta': 1.0e-5 }, 'stresses': { 'local_virial': None }, 'elastic_constants': {}, 'unrelaxed_elastic_constants': {} } # list of properties that require a call to Potential.calc() calc_properties = [ 'energy', 'energies', 'forces', 'numeric_forces', 'stress', 'stresses' ] # list of other properties we know how to calculate other_properties = ['elastic_constants', 'unrelaxed_elastic_constants'] calc_args = {} calc_required = False for property in properties: if property in calc_properties: calc_required = True calc_args.update(args_map[property]) elif property not in other_properties: raise RuntimeError( "Don't know how to calculate property '%s'" % property) if calc_required: self.calc(self.quippy_atoms, args_str=dict_to_args_str(calc_args)) if 'energy' in properties: self.results['energy'] = float(self.quippy_atoms.energy) if 'energies' in properties: self.results['energies'] = self.quippy_atoms.local_energy.copy( ).view(np.ndarray) if 'forces' in properties: self.results['forces'] = self.quippy_atoms.force.copy().view( np.ndarray).T if 'numeric_forces' in properties: self.results[ 'numeric_forces'] = self.quippy_atoms.numeric_force.copy( ).view(np.ndarray).T if 'stress' in properties: stress = -self.quippy_atoms.virial.copy().view( np.ndarray) / self.quippy_atoms.get_volume() # convert to 6-element array in Voigt order self.results['stress'] = np.array([ stress[0, 0], stress[1, 1], stress[2, 2], stress[1, 2], stress[0, 2], stress[0, 1] ]) if 'stresses' in properties: lv = np.array(self.quippy_atoms.local_virial) # make a copy vol_per_atom = self.get( 'vol_per_atom', self.quippy_atoms.get_volume() / len(atoms)) if isinstance(vol_per_atom, basestring): vol_per_atom = self.quippy_atoms.arrays[vol_per_atom] self.results['stresses'] = -lv.T.reshape( (len(atoms), 3, 3), order='F') / vol_per_atom if 'elastic_constants' in properties: cij_dx = self.get('cij_dx', 1e-2) cij = fzeros((6, 6)) self.calc_elastic_constants(self.quippy_atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c=cij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): cij = cij.view(np.ndarray) self.results['elastic_constants'] = cij if 'unrelaxed_elastic_constants' in properties: cij_dx = self.get('cij_dx', 1e-2) c0ij = fzeros((6, 6)) self.calc_elastic_constants(self.quippy_atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c0=c0ij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): c0ij = c0ij.view(np.ndarray) self.results['unrelaxed_elastic_constants'] = c0ij # copy back any additional output data to results dictionary skip_keys = ['energy', 'force', 'virial', 'numeric_force'] for key in self.quippy_atoms.arrays.keys(): if key not in initial_arrays and key not in skip_keys: self.results[key] = self.quippy_atoms.arrays[key].copy() for key in self.quippy_atoms.info.keys(): if key not in initial_info and key not in skip_keys: if isinstance(self.quippy_atoms.info[key], np.ndarray): self.results[key] = self.quippy_atoms.info[key].copy() else: self.results[key] = self.quippy_atoms.info[key] def get_potential_energies(self, atoms): """ Return array of atomic energies calculated with this Potential """ return self.get_property('energies', atoms) def get_numeric_forces(self, atoms): """ Return forces on `atoms` computed with finite differences of the energy """ return self.get_property('numeric_forces', atoms) def get_stresses(self, atoms): """ Return the per-atoms virial stress tensors for `atoms` computed with this Potential """ return self.get_property('stresses', atoms) def get_elastic_constants(self, atoms): """ Calculate elastic constants of `atoms` using this Potential. Returns 6x6 matrix :math:`C_{ij}` of elastic constants. The elastic contants are calculated as finite difference derivatives of the virial stress tensor using positive and negative strains of magnitude the `cij_dx` entry in ``calc_args``. """ return self.get_property('elastic_constants', atoms) def get_unrelaxed_elastic_constants(self, atoms): """ Calculate unrelaxed elastic constants of `atoms` using this Potential Returns 6x6 matrix :math:`C^0_{ij}` of unrelaxed elastic constants. The elastic contants are calculated as finite difference derivatives of the virial stress tensor using positive and negative strains of magnitude the `cij_dx` entry in :attr:`calc_args`. """ return self.get_property('unrelaxed_elastic_constants', atoms) def get_default_properties(self): "Get the list of properties to be calculated by default" return self._default_properties[:] def set_default_properties(self, properties): "Set the list of properties to be calculated by default" self._default_properties = properties[:] def get(self, param, default=None): """ Get the value of a ``calc_args`` parameter for this :class:`Potential` Returns ``None`` if `param` is not in the current ``calc_args`` dictionary. All calc_args are passed to :meth:`calc` whenever energies, forces or stresses need to be re-computed. """ return self._calc_args.get(param, default) def set(self, **kwargs): """ Set one or more calc_args parameters for this Potential All calc_args are passed to :meth:`calc` whenever energies, forces or stresses need to be computed. After updating the calc_args, :meth:`set` calls :meth:`reset` to mark all properties as needing to be recaculated. """ self._calc_args.update(kwargs) self.reset() def get_calc_args(self): """ Get the current ``calc_args`` """ return self._calc_args.copy() def set_calc_args(self, calc_args): """ Set the ``calc_args`` to be used subsequent :meth:`calc` calls """ self._calc_args = calc_args.copy() def get_calc_args_str(self): """ Get the ``calc_args`` to be passed to :meth:`calc` as a string """ return dict_to_args_str(self._calc_args)
def CP2KOutputReader(fh, module=None, type_map=None, kind_map=None, format=None): # mapping from run type to (default module index, list of available module) run_types = { 'QS': ['QUICKSTEP'], 'QMMM': ['FIST', 'QM/MM', 'QUICKSTEP'], 'MM': ['FIST'] } filename, lines = read_text_file(fh) run_type = cp2k_run_type(cp2k_output=lines) if type_map is None: type_map = {} if kind_map is None: kind_map = {} try: available_modules = run_types[run_type] except KeyError: raise ValueError('Unknown CP2K run type %s' % run_type) if module is None: module = available_modules[0] try: cell_index = available_modules.index(module) except ValueError: raise ValueError("Don't know how to read module %s from file %s" % (module, filename)) cell_lines = [ i for i, line in enumerate(lines) if line.startswith(" CELL| Vector a") ] if cell_lines == []: raise ValueError("Cannot find cell in file %s" % filename) try: cell_line = cell_lines[cell_index] except IndexError: raise ValueError( "Cannot find cell with index %d in file %s for module %s" % (cell_index, filename, module)) lattice = fzeros((3, 3)) for i in [0, 1, 2]: lattice[:, i + 1] = [float(c) for c in lines[cell_line + i].split()[4:7]] try: start_line = lines.index( " MODULE %s: ATOMIC COORDINATES IN angstrom\n" % module) except ValueError: raise ValueError( "Cannot find atomic positions for module %s in file %s" % (module, filename)) kinds = [] species = [] Zs = [] pos = [] masses = [] Zeffs = [] types = [] qeffs = [] for line in lines[start_line + 4:]: if line.strip() == '': break if module == 'FIST': atom, kind, typ, x, y, z, qeff, mass = line.split() types.append(typ) Z = type_map.get(typ, 0) kind = int(kind) if Z == 0: Z = kind_map.get(kind, 0) Zs.append(Z) qeffs.append(float(qeff)) else: atom, kind, sp, Z, x, y, z, Zeff, mass = line.split() species.append(sp) Zs.append(int(Z)) Zeffs.append(float(Zeff)) kinds.append(int(kind)) pos.append([float(x), float(y), float(z)]) masses.append(float(mass)) at = Atoms(n=len(kinds), lattice=lattice) at.pos[...] = farray(pos).T at.set_atoms(Zs) at.add_property('mass', farray(masses) * MASSCONVERT) at.add_property('kind', kinds) if module == 'FIST': at.add_property('type', ' ' * TABLE_STRING_LENGTH) at.add_property('qm', False) at.qm[:] = (at.type.stripstrings() == '_QM_') | (at.type.stripstrings() == '_LNK') at.type[...] = s2a(types, TABLE_STRING_LENGTH) at.add_property('qeff', qeffs) else: at.species[...] = s2a(species, TABLE_STRING_LENGTH) at.add_property('zeff', Zeffs) yield at
parser.add_argument("-st", "--sim_T", help='Simulation Temperature in Kelvin. Default is 300 K.', type=float, required=True) parser.add_argument("-cfe", "--check_force_error", help='Perform a DFT calculation at each step in the trajectory.', action='store_true') args = parser.parse_args() # parse args string: if args.input_file == '': input_file = params.input_file else: input_file = args.input_file # ParseArguements sim_T = args.sim_T*units.kB geom = args.geom check_force_error = args.check_force_error print 'Loading atoms from file %s' % input_file atoms = Atoms(input_file) atoms = Atoms(atoms) if params.continuation: # restart from last frame of most recent trajectory file traj_files = sorted(glob.glob('[0-9]*.traj.xyz')) if len(traj_files) > 0: last_traj = traj_files[-1] input_file = last_traj + '@-1' # loading reference configuration for Nye tensor evaluation # convert to quippy Atoms - FIXME in long term, this should not be necesary x0 = Atoms(params.reference_file) x0 = Atoms(x0) x0.set_cutoff(3.0) x0.calc_connect()
sim_T = 300.0*units.kB # Simulation temperature nsteps = 10000 # Total number of timesteps to run for timestep = 1.0*units.fs # Timestep (NB: time base units are not fs!) cutoff_skin = 2.0*units.Ang # Amount by which potential cutoff is increased # for neighbour calculations tip_move_tol = 10.0 # Distance tip has to move before crack # is taken to be running strain_rate = 1e-5*(1/units.fs) # Strain rate traj_file = 'traj.nc' # Trajectory output file (NetCDF format) traj_interval = 10 # Number of time steps between # writing output frames set_fortran_indexing(False) atoms = Atoms(input_file) orig_height = atoms.info['OrigHeight'] orig_crack_pos = atoms.info['CrackPos'].copy() top = atoms.positions[:, 1].max() bottom = atoms.positions[:, 1].min() left = atoms.positions[:, 0].min() right = atoms.positions[:, 0].max() fixed_mask = ((abs(atoms.positions[:, 1] - top) < 1.0) | (abs(atoms.positions[:, 1] - bottom) < 1.0)) fix_atoms = FixAtoms(mask=fixed_mask) strain_atoms = ConstantStrainRate(orig_height, strain_rate*timestep) atoms.set_constraint([fix_atoms, strain_atoms])
class Potential(_potential.Potential): __doc__ = update_doc_string( _potential.Potential.__doc__, r""" The :class:`Potential` class also implements the ASE :class:`ase.calculators.interface.Calculator` interface via the the :meth:`get_forces`, :meth:`get_stress`, :meth:`get_stresses`, :meth:`get_potential_energy`, :meth:`get_potential_energies` methods. This simplifies calculation since there is no need to set the cutoff or to call :meth:`~quippy.atoms.Atoms.calc_connect`, as this is done internally. The example above reduces to:: atoms = diamond(5.44, 14) atoms.rattle(0.01) atoms.set_calculator(pot) forces = atoms.get_forces() print forces Note that the ASE force array is the transpose of the QUIP force array, so has shape (len(atoms), 3) rather than (3, len(atoms)). The optional arguments `pot1`, `pot2` and `bulk_scale` are used by ``Sum`` and ``ForceMixing`` potentials (see also wrapper class :class:`ForceMixingPotential`) An :class:`quippy.mpi_context.MPI_context` object can be passed as the `mpi_obj` argument to restrict the parallelisation of this potential to a subset of the The `callback` argument is used to implement the calculation of the :class:`Potential` in a Python function: see :meth:`set_callback` for an example. In addition to the builtin QUIP potentials, it is possible to use any ASE calculator as a QUIP potential by passing it as the `calculator` argument to the :class:`Potential` constructor, e.g.:: from ase.calculators.morse import MorsePotential pot = Potential(calculator=MorsePotential) `cutoff_skin` is used to set the :attr:`cutoff_skin` attribute. `atoms` if given, is used to set the calculator associated with `atoms` to the new :class:`Potential` instance, by calling :meth:'.Atoms.set_calculator`. .. note:: QUIP potentials do not compute stress and per-atom stresses directly, but rather the virial tensor which has units of stress :math:`\times` volume, i.e. energy. If the total stress is requested, it is computed by dividing the virial by the atomic volume, obtained by calling :meth:`.Atoms.get_volume`. If per-atom stresses are requested, a per-atom volume is needed. By default this is taken to be the total volume divided by the number of atoms. In some cases, e.g. for systems containing large amounts of vacuum, this is not reasonable. The ``vol_per_atom`` calc_arg can be used either to give a single per-atom volume, or the name of an array in :attr:`.Atoms.arrays` containing volumes for each atom. """, signature= 'Potential(init_args[, pot1, pot2, param_str, param_filename, bulk_scale, mpi_obj, callback, calculator, cutoff_skin, atoms])' ) callback_map = {} def __init__(self, init_args=None, pot1=None, pot2=None, param_str=None, param_filename=None, bulk_scale=None, mpi_obj=None, callback=None, calculator=None, cutoff_skin=1.0, atoms=None, fpointer=None, finalise=True, error=None, **kwargs): self.atoms = None self._prev_atoms = None self.energy = None self.energies = None self.forces = None self.stress = None self.stresses = None self.elastic_constants = None self.unrelaxed_elastic_constants = None self.numeric_forces = None self._calc_args = {} self._default_quantities = [] self.cutoff_skin = cutoff_skin if callback is not None or calculator is not None: if init_args is None: init_args = 'callbackpot' if param_filename is not None: param_str = open(param_filename).read() if init_args is None and param_str is None: raise ValueError('Need one of init_args,param_str,param_filename') if init_args is not None: if init_args.lower().startswith('callbackpot'): if not 'label' in init_args: init_args = init_args + ' label=%d' % id(self) else: # if param_str missing, try to find default set of QUIP params if param_str is None and pot1 is None and pot2 is None: param_str = quip_xml_parameters(init_args) if kwargs != {}: if init_args is not None: init_args = init_args + ' ' + dict_to_args_str(kwargs) else: init_args = dict_to_args_str(kwargs) _potential.Potential.__init__(self, init_args, pot1=pot1, pot2=pot2, param_str=param_str, bulk_scale=bulk_scale, mpi_obj=mpi_obj, fpointer=fpointer, finalise=finalise, error=error) if init_args is not None and init_args.lower().startswith( 'callbackpot'): _potential.Potential.set_callback(self, Potential.callback) if callback is not None: self.set_callback(callback) if calculator is not None: self.set_callback(calculator_callback_factory(calculator)) if atoms is not None: atoms.set_calculator(self) __init__.__doc__ = _potential.Potential.__init__.__doc__ def calc(self, at, energy=None, force=None, virial=None, local_energy=None, local_virial=None, args_str=None, error=None, **kwargs): if not isinstance(args_str, basestring): args_str = dict_to_args_str(args_str) kw_args_str = dict_to_args_str(kwargs) args_str = ' '.join((self.get_calc_args_str(), kw_args_str, args_str)) if isinstance(energy, basestring): args_str = args_str + ' energy=%s' % energy energy = None if isinstance(energy, bool) and energy: args_str = args_str + ' energy' energy = None if isinstance(force, basestring): args_str = args_str + ' force=%s' % force force = None if isinstance(force, bool) and force: args_str = args_str + ' force' force = None if isinstance(virial, basestring): args_str = args_str + ' virial=%s' % virial virial = None if isinstance(virial, bool) and virial: args_str = args_str + ' virial' virial = None if isinstance(local_energy, basestring): args_str = args_str + ' local_energy=%s' % local_energy local_energy = None if isinstance(local_energy, bool) and local_energy: args_str = args_str + ' local_energy' local_energy = None if isinstance(local_virial, basestring): args_str = args_str + ' local_virial=%s' % local_virial local_virial = None if isinstance(local_virial, bool) and local_virial: args_str = args_str + ' local_virial' local_virial = None potlog.debug( 'Potential invoking calc() on n=%d atoms with args_str "%s"' % (len(at), args_str)) _potential.Potential.calc(self, at, energy, force, virial, local_energy, local_virial, args_str, error) calc.__doc__ = update_doc_string( _potential.Potential.calc.__doc__, """In Python, this method is overloaded to set the final args_str to :meth:`get_calc_args_str`, followed by any keyword arguments, followed by an explicit `args_str` argument if present. This ordering ensures arguments explicitly passed to :meth:`calc` will override any default arguments.""") @staticmethod def callback(at_ptr): from quippy import Atoms at = Atoms(fpointer=at_ptr, finalise=False) if 'label' not in at.params or at.params[ 'label'] not in Potential.callback_map: raise ValueError('Unknown Callback label %s' % at.params['label']) Potential.callback_map[at.params['label']](at) def set_callback(self, callback): """ For a :class:`Potential` of type `CallbackPot`, this method is used to set the callback function. `callback` should be a Python function (or other callable, such as a bound method or class instance) which takes a single argument, of type :class:`~quippy.atoms.Atoms`. Information about which quantities should be computed can be obtained from the `calc_energy`, `calc_local_e`, `calc_force`, and `calc_virial` keys in `at.params`. Results should be returned either as `at.params` entries (for energy and virial) or by adding new atomic properties (for forces and local energy). Here's an example implementation of a simple callback:: def example_callback(at): if at.calc_energy: at.params['energy'] = ... if at.calc_force: at.add_property('force', 0.0, n_cols=3) at.force[:,:] = ... p = Potential('CallbackPot') p.set_callback(example_callback) p.calc(at, energy=True) print at.energy ... """ Potential.callback_map[str(id(self))] = callback def wipe(self): """ Mark all quantities as needing to be recalculated """ self.energy = None self.energies = None self.forces = None self.stress = None self.stresses = None self.numeric_forces = None self.elastic_constants = None self.unrelaxed_elastic_constants = None def update(self, atoms): """ Set the :class:`~quippy.atoms.Atoms` object associated with this :class:`Potential` to `atoms`. Called internally by :meth:`get_potential_energy`, :meth:`get_forces`, etc. Only a weak reference to `atoms` is kept, to prevent circular references. If `atoms` is not a :class:`quippy.atoms.Atoms` instance, then a copy is made and a warning will be printed. """ # we will do the calculation in place, to minimise number of copies, # unless atoms is not a quippy Atoms if isinstance(atoms, Atoms): self.atoms = weakref.proxy(atoms) else: potlog.debug( 'Potential atoms is not quippy.Atoms instance, copy forced!') self.atoms = Atoms(atoms) # check if atoms has changed since last call if self._prev_atoms is not None and self._prev_atoms.equivalent( self.atoms): return # Mark all quantities as needing to be recalculated self.wipe() # do we need to reinitialise _prev_atoms? if self._prev_atoms is None or len(self._prev_atoms) != len( self.atoms) or not self.atoms.connect.initialised: self._prev_atoms = Atoms() self._prev_atoms.copy_without_connect(self.atoms) self._prev_atoms.add_property('orig_pos', self.atoms.pos) else: # _prev_atoms is OK, update it in place self._prev_atoms.z[...] = self.atoms.z self._prev_atoms.pos[...] = self.atoms.pos self._prev_atoms.lattice[...] = self.atoms.lattice # do a calc_connect(), setting cutoff_skin so full reconnect will only be done when necessary self.atoms.set_cutoff(self.cutoff(), cutoff_skin=self.cutoff_skin) potlog.debug( 'Potential doing calc_connect() with cutoff %f cutoff_skin %r' % (self.atoms.cutoff, self.cutoff_skin)) self.atoms.calc_connect() # Synonyms for `update` for compatibility with ASE calculator interface def initialize(self, atoms): self.update(atoms) def set_atoms(self, atoms): self.update(atoms) def calculation_required(self, atoms, quantities): self.update(atoms) for quantity in quantities: if getattr(self, quantity) is None: return True return False def calculate(self, atoms, quantities=None): """ Perform a calculation of `quantities` for `atoms` using this Potential. Automatically determines if a new calculation is required or if previous results are still appliciable (i.e. if the atoms haven't moved since last call) Called internally by :meth:`get_potential_energy`, :meth:`get_forces`, etc. """ if quantities is None: quantities = ['energy', 'forces', 'stress'] # Add any default quantities quantities = set(self.get_default_quantities() + quantities) if len(quantities) == 0: raise RuntimeError('Nothing to calculate') if not self.calculation_required(atoms, quantities): return args_map = { 'energy': { 'energy': None }, 'energies': { 'local_energy': None }, 'forces': { 'force': None }, 'stress': { 'virial': None }, 'numeric_forces': { 'force': 'numeric_force', 'force_using_fd': True, 'force_fd_delta': 1.0e-5 }, 'stresses': { 'local_virial': None }, 'elastic_constants': {}, 'unrelaxed_elastic_constants': {} } # list of quantities that require a call to Potential.calc() calc_quantities = [ 'energy', 'energies', 'forces', 'numeric_forces', 'stress', 'stresses' ] # list of other quantities we know how to calculate other_quantities = ['elastic_constants', 'unrelaxed_elastic_constants'] calc_args = {} calc_required = False for quantity in quantities: if quantity in calc_quantities: calc_required = True calc_args.update(args_map[quantity]) elif quantity not in other_quantities: raise RuntimeError( "Don't know how to calculate quantity '%s'" % quantity) if calc_required: self.calc(self.atoms, args_str=dict_to_args_str(calc_args)) if 'energy' in quantities: self.energy = float(self.atoms.energy) if 'energies' in quantities: self.energies = self.atoms.local_energy.view(np.ndarray) if 'forces' in quantities: self.forces = self.atoms.force.view(np.ndarray).T if 'numeric_forces' in quantities: self.numeric_forces = self.atoms.numeric_force.view(np.ndarray).T if 'stress' in quantities: stress = -self.atoms.virial.view( np.ndarray) / self.atoms.get_volume() # convert to 6-element array in Voigt order self.stress = np.array([ stress[0, 0], stress[1, 1], stress[2, 2], stress[1, 2], stress[0, 2], stress[0, 1] ]) if 'stresses' in quantities: lv = np.array(self.atoms.local_virial) # make a copy vol_per_atom = self.get('vol_per_atom', self.atoms.get_volume() / len(atoms)) if isinstance(vol_per_atom, basestring): vol_per_atom = self.atoms.arrays[vol_per_atom] self.stresses = -lv.T.reshape( (len(atoms), 3, 3), order='F') / vol_per_atom if 'elastic_constants' in quantities: cij_dx = self.get('cij_dx', 1e-2) cij = fzeros((6, 6)) self.calc_elastic_constants(self.atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c=cij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): cij = cij.view(np.ndarray) self.elastic_constants = cij if 'unrelaxed_elastic_constants' in quantities: cij_dx = self.get('cij_dx', 1e-2) c0ij = fzeros((6, 6)) self.calc_elastic_constants(self.atoms, fd=cij_dx, args_str=self.get_calc_args_str(), c0=c0ij, relax_initial=False, return_relaxed=False) if not get_fortran_indexing(): c0ij = c0ij.view(np.ndarray) self.unrelaxed_elastic_constants = c0ij def get_potential_energy(self, atoms): """ Return potential energy of `atoms` calculated with this Potential """ self.calculate(atoms, ['energy']) return self.energy def get_potential_energies(self, atoms): """ Return array of atomic energies calculated with this Potential """ self.calculate(atoms, ['energies']) return self.energies.copy() def get_forces(self, atoms): """ Return forces on `atoms` calculated with this Potential """ self.calculate(atoms, ['forces']) return self.forces.copy() def get_numeric_forces(self, atoms): """ Return forces on `atoms` computed with finite differences of the energy """ self.calculate(atoms, ['numeric_forces']) return self.numeric_forces.copy() def get_stress(self, atoms): """ Return stress tensor for `atoms` computed with this Potential Result is a 6-element array in Voigt notation: [sigma_xx, sigma_yy, sigma_zz, sigma_yz, sigma_xz, sigma_xy] """ self.calculate(atoms, ['stress']) return self.stress.copy() def get_stresses(self, atoms): """ Return the per-atoms virial stress tensors for `atoms` computed with this Potential """ self.calculate(atoms, ['stresses']) return self.stresses.copy() def get_elastic_constants(self, atoms): """ Calculate elastic constants of `atoms` using this Potential. Returns 6x6 matrix :math:`C_{ij}` of elastic constants. The elastic contants are calculated as finite difference derivatives of the virial stress tensor using positive and negative strains of magnitude the `cij_dx` entry in ``calc_args``. """ self.calculate(atoms, ['elastic_constants']) return self.elastic_constants.copy() def get_unrelaxed_elastic_constants(self, atoms): """ Calculate unrelaxed elastic constants of `atoms` using this Potential Returns 6x6 matrix :math:`C^0_{ij}` of unrelaxed elastic constants. The elastic contants are calculated as finite difference derivatives of the virial stress tensor using positive and negative strains of magnitude the `cij_dx` entry in :attr:`calc_args`. """ self.calculate(atoms, ['unrelaxed_elastic_constants']) return self.unrelaxed_elastic_constants.copy() def get_default_quantities(self): "Get the list of quantities to be calculated by default" return self._default_quantities[:] def set_default_quantities(self, quantities): "Set the list of quantities to be calculated by default" self._default_quantities = quantities[:] def get(self, param, default=None): """ Get the value of a ``calc_args`` parameter for this :class:`Potential` Returns ``None`` if `param` is not in the current ``calc_args`` dictionary. All calc_args are passed to :meth:`calc` whenever energies, forces or stresses need to be re-computed. """ return self._calc_args.get(param, default) def set(self, **kwargs): """ Set one or more calc_args parameters for this Potential All calc_args are passed to :meth:`calc` whenever energies, forces or stresses need to be computed. After updating the calc_args, :meth:`set` calls :meth:`wipe` to mark all quantities as needing to be recaculated. """ self._calc_args.update(kwargs) self.wipe() def get_calc_args(self): """ Get the current ``calc_args`` """ return self._calc_args.copy() def set_calc_args(self, calc_args): """ Set the ``calc_args`` to be used subsequent :meth:`calc` calls """ self._calc_args = calc_args.copy() def get_calc_args_str(self): """ Get the ``calc_args`` to be passed to :meth:`calc` as a string """ return dict_to_args_str(self._calc_args) def get_cutoff_skin(self): return self._cutoff_skin def set_cutoff_skin(self, cutoff_skin): self._cutoff_skin = cutoff_skin self._prev_atoms = None # force a recalculation cutoff_skin = property(get_cutoff_skin, set_cutoff_skin, doc=""" The `cutoff_skin` attribute is only relevant when the ASE-style interface to the Potential is used, via the :meth:`get_forces`, :meth:`get_potential_energy` etc. methods. In this case the connectivity of the :class:`~quippy.atoms.Atoms` object for which the calculation is requested is automatically kept up to date by using a neighbour cutoff of :meth:`cutoff` + `cutoff_skin`, and recalculating the neighbour lists whenever the maximum displacement since the last :meth:`Atoms.calc_connect` exceeds `cutoff_skin`. """)