def test_multiple_elements(self): a = molecule('HCOOH') a.center(vacuum=5.0) io.write('HCOOH.cfg', a) i = neighbour_list("i", a, 1.85) self.assertArrayAlmostEqual(np.bincount(i), [2, 3, 1, 1, 1]) cutoffs = np.zeros([9, 9]) cutoffs[1, 6] = cutoffs[6, 1] = 1.2 i = neighbour_list("i", a, cutoffs, np.array(a.numbers, dtype=np.int32)) self.assertArrayAlmostEqual(np.bincount(i), [0, 1, 0, 0, 1]) cutoffs = np.zeros([9, 9]) cutoffs[6, 8] = cutoffs[8, 6] = 1.4 i = neighbour_list("i", a, cutoffs, np.array(a.numbers, dtype=np.int32)) self.assertArrayAlmostEqual(np.bincount(i), [1, 2, 1]) cutoffs = np.zeros([9, 9]) cutoffs[1, 6] = cutoffs[6, 1] = 1.2 cutoffs[6, 8] = cutoffs[8, 6] = 1.4 i = neighbour_list("i", a, cutoffs, np.array(a.numbers, dtype=np.int32)) self.assertArrayAlmostEqual(np.bincount(i), [1, 3, 1, 0, 1])
def test_out_of_bounds(self): nat = 10 atoms = ase.Atoms(numbers=range(nat), cell=[(0.2, 1.2, 1.4), (1.4, 0.1, 1.6), (1.3, 2.0, -0.1)]) atoms.set_scaled_positions(3 * np.random.random((nat, 3)) - 1) for p1 in range(2): for p2 in range(2): for p3 in range(2): atoms.set_pbc((p1, p2, p3)) i, j, d, D, S = neighbour_list("ijdDS", atoms, atoms.numbers * 0.2 + 0.5) c = np.bincount(i) atoms2 = atoms.repeat((p1 + 1, p2 + 1, p3 + 1)) i2, j2, d2, D2, S2 = neighbour_list( "ijdDS", atoms2, atoms2.numbers * 0.2 + 0.5) c2 = np.bincount(i2) c2.shape = (-1, nat) dd = d.sum() * (p1 + 1) * (p2 + 1) * (p3 + 1) - d2.sum() dr = np.linalg.solve(atoms.cell.T, (atoms.positions[1] - atoms.positions[0]).T).T + np.array( [0, 0, 3]) self.assertTrue(abs(dd) < 1e-10) self.assertTrue(not (c2 - c).any())
def test_out_of_cell_large_cell(self): a = ase.Atoms('CC', positions=[[9.5, 0.5, 0.5], [10.1, 0.5, 0.5]], cell=[10, 10, 10], pbc=False) i1, j1, r1 = neighbour_list("ijd", a, 1.1) a.set_cell([20, 10, 10]) i2, j2, r2 = neighbour_list("ijd", a, 1.1) self.assertArrayAlmostEqual(i1, i2) self.assertArrayAlmostEqual(j1, j2) self.assertArrayAlmostEqual(r1, r2)
def find_triangles_2d(atoms, cutoff, minangle=30*np.pi/180, maxangle=120*np.pi/180, xdim=0, ydim=1): """ Return a list of all triangles of a triangular lattice sitting in the x-y plane. """ # Contains atom indices that border the triangle corner1 = [] corner2 = [] corner3 = [] # Find triangles i, j, D = neighbour_list('ijD', atoms, cutoff) coord = np.bincount(i) for k in range(len(atoms)): firstn = np.searchsorted(i, k, side='left') lastn = np.searchsorted(i, k, side='right') # Sort six neighbors by angle angles = np.arctan2(D[firstn:lastn, xdim], D[firstn:lastn, ydim]) s = np.argsort(angles) # Only pick triangles with angles between min and max angle trangles = (angles[np.roll(s, -1)]-angles[s]) % (2*np.pi) m = (trangles > minangle) & (trangles < maxangle) # Add corners of triangle to lists corner1 += list(np.array([k]*(lastn-firstn))[m]) corner2 += list(j[firstn:lastn][s][m]) corner3 += list(j[firstn:lastn][np.roll(s, -1)][m]) # Sort corners corner1, corner2, corner3 = np.sort([corner1, corner2, corner3], axis=0) # Remove duplicate triangles uniqueid = corner3+len(atoms)*(corner2+len(atoms)*corner1) _, s = np.unique(uniqueid, return_index=True) return corner1[s], corner2[s], corner3[s]
def remove_dangling_atoms(atoms, keep_cell=False, write=False): if not keep_cell: cell0 = atoms.get_cell() atoms.set_cell([1000] * 3) atoms.set_pbc([True] * 3) # atoms.set_cutoff(2.) # atoms.calc_connect() # remove dangling atoms not in loops dropped = 1 idx = 0 while dropped > 0: # nneighs = np.array([len(atoms.neighbours[i]) for i in atoms.indices]) # quip implementation is slow ni = neighbour_list("i", atoms, 2.0) nneighs = np.array([(ni == i).sum() for i in range(len(atoms))]) speciesSi = atoms.get_atomic_numbers() == 14 mask = np.array([(nneigh < 3) if issilicon else (nneigh < 2) for issilicon, nneigh in zip(speciesSi, nneighs)]) if write: atoms.set_array("remove", mask) atoms.write("rem-%03d.xyz" % idx, format="extxyz") if type(atoms) == QAtoms: atoms.remove_atoms(mask=mask) else: del atoms[np.where(mask)] dropped = mask.sum() idx += 1 if not keep_cell: atoms.set_cell(cell0) return
def test_hydrogenate(self): a = Diamond('Si', size=[2,2,1]) b = hydrogenate(a, 2.85, 1.0, mask=[True,True,False], vacuum=5.0) # Check if every atom is fourfold coordinated syms = np.array(b.get_chemical_symbols()) c = np.bincount(neighbour_list('i', b, 2.4)) self.assertTrue((c[syms!='H']==4).all())
def test_hexagonal_cell(self): for sx in range(3): a = ase.lattice.hexagonal.Graphite('C', latticeconstant=(2.5, 10.0), size=[sx + 1, sx + 1, 1]) i = neighbour_list("i", a, 1.85) self.assertTrue(np.all(np.bincount(i) == 3))
def atoms_to_nxgraph(atoms, cutoff): ni, nj = neighbour_list('ij', atoms, cutoff) adjacency_matrix = np.zeros((len(atoms), len(atoms))).astype(np.int) for i, j in zip (ni, nj): adjacency_matrix[i,j] = 1 graph = nx.from_numpy_matrix(np.array(adjacency_matrix)) return graph
def find_tip_coordination(a, bondlength=2.6, bulk_nn=4): """ Find position of tip in crack cluster from coordination """ i, j = neighbour_list("ij", a, bondlength) nn = np.bincount(i, minlength=len(a)) a.set_array('n_neighb', nn) g = a.get_array('groups') y = a.positions[:, 1] above = (nn < bulk_nn) & (g != 0) & (y > a.cell[1,1]/2.0) below = (nn < bulk_nn) & (g != 0) & (y < a.cell[1,1]/2.0) a.set_array('above', above) a.set_array('below', below) bond1 = np.asscalar(above.nonzero()[0][a.positions[above, 0].argmax()]) bond2 = np.asscalar(below.nonzero()[0][a.positions[below, 0].argmax()]) # These need to be ints, otherwise they are no JSON serializable. a.info['bond1'] = bond1 a.info['bond2'] = bond2 return bond1, bond2
def spatial_correlation_function_near(atoms, values, gridsize=None, cutoff=None, norm=False): if gridsize is None: gridsize = 0.1 if cutoff is None: cutoff = 7.5 # close range exact calculation nbins = int(cutoff / gridsize) + 1 index1, index2, dist = neighbour_list('ijd', atoms, cutoff=cutoff) SCF_near, edges = np.histogram(dist, bins=bins, weights=values[index1] * values[index2]) slice_volume = 4 * np.pi / 3 * (edges[1:]**3 - edges[:-1]**3) SCF_near *= atoms.get_volume() / n_atoms**2 / slice_volume if norm: v_2_mean = (values**2).mean() v_mean_2 = (values.mean())**2 SCF_near = (SCF_near - v_mean_2) / (v_2_mean - v_mean_2) return SCF_near, (edges[1:] + edges[:-1]) / 2
def test_hydrogenate(self): a = Diamond('Si', size=[2, 2, 1]) b = hydrogenate(a, 2.85, 1.0, mask=[True, True, False], vacuum=5.0) # Check if every atom is fourfold coordinated syms = np.array(b.get_chemical_symbols()) c = np.bincount(neighbour_list('i', b, 2.4)) self.assertTrue((c[syms != 'H'] == 4).all())
def test_anisotropic_near_field_solution(self): """ Run an atomistic calculation of a harmonic solid and compare to continuum solution. """ for nx in [ 8, 16, 32, 64 ]: for calc, a, C11, C12, C44, surface_energy, bulk_coordination in [ #( atomistica.DoubleHarmonic(k1=1.0, r1=1.0, k2=1.0, # r2=math.sqrt(2), cutoff=1.6), # clusters.sc('He', 1.0, [nx,nx,1], [1,0,0], [0,1,0]), # 3, 1, 1, 0.05, 6 ), ( #atomistica.Harmonic(k=1.0, r0=1.0, cutoff=1.3, shift=True), IdealBrittleSolid(k=1.0, a=1.0, rc=1.3), clusters.fcc('He', math.sqrt(2.0), [nx,nx,1], [1,0,0], [0,1,0]), math.sqrt(2), 1.0/math.sqrt(2), 1.0/math.sqrt(2), 0.05, 12) ]: clusters.set_groups(a, (nx, nx, 1), 0.5, 0.5) crack = CubicCrystalCrack([1,0,0], [0,1,0], C11, C12, C44) a.center(vacuum=20.0, axis=0) a.center(vacuum=20.0, axis=1) a.set_calculator(calc) sx, sy, sz = a.cell.diagonal() tip_x = sx/2 tip_y = sy/2 k1g = crack.k1g(surface_energy) r0 = a.positions.copy() u, v = crack.displacements(a.positions[:,0], a.positions[:,1], tip_x, tip_y, k1g) a.positions[:,0] += u a.positions[:,1] += v g = a.get_array('groups') a.set_constraint(FixAtoms(mask=g==0)) #ase.io.write('initial_{}.xyz'.format(nx), a, format='extxyz', write_results=False) x1, y1, z1 = a.positions.copy().T FIRE(a, logfile=None).run(fmax=1e-3) x2, y2, z2 = a.positions.T # Get coordination numbers and find properly coordinated atoms i = neighbour_list("i", a, 1.1) coord = np.bincount(i, minlength=len(a)) mask=coord == bulk_coordination residual = np.sqrt(((x2-x1)/u)**2 + ((y2-y1)/v)**2) #a.set_array('residual', residual) #ase.io.write('final_{}.xyz'.format(nx), a, format='extxyz') #print(np.max(residual[mask])) self.assertTrue(np.max(residual[mask]) < 0.2)
def test_no_angle(self): a = ase.Atoms('CC', positions=[[0.5, 0.5, 0.5], [0.5, 0.5, 1.0]], cell=[2, 2, 2], pbc=True) i, j, dr = neighbour_list("ijD", a, 1.1) hist = angle_distribution(i, j, dr, 20, 1.1) self.assertEqual(hist.sum(), 0)
def view(a, colour=None, bonds=True, cell=True, scale=10.0, cutoff_scale=1.2, cmap=None, vmin=None, vmax=None): topology = {} topology['atom_types'] = a.get_chemical_symbols() if bonds: n = a.numbers maxn = n.max() cutoffs = np.zeros([maxn+1, maxn+1]) for n1, n2 in itertools.product(n, n): cutoffs[n1, n2] = cutoff_scale*(covalent_radii[n1]+covalent_radii[n2]) # Construct a bond list i, j, S = neighbour_list('ijS', a, cutoffs, np.array(a.numbers, dtype=np.int32)) m = np.logical_and(i<j, (S==0).all(axis=1)) i = i[m] j = j[m] topology['bonds'] = [(x, y) for x, y in zip(i, j)] colorlist = None if colour is not None: colour = np.array(colour, dtype=np.float64) if cmap is None: from matplotlib.cm import jet cmap = jet if vmin is None: vmin = np.min(colour) if vmax is None: vmax = np.max(colour) colour = (colour - vmin)/(vmax - vmin) colorlist = ['0x%02x%02x%02x' % (r*256, g*256, b*256) for (r, g, b, alpha) in cmap(colour)] mv = MolecularViewer(a.positions/scale, topology=topology) mv.ball_and_sticks(colorlist=colorlist) if cell: O = np.zeros(3, dtype=np.float32) La, Lb, Lc = a.cell.astype(np.float32)/scale start = np.r_[O, O, O, O + Lb, O + Lc, O + La, O + Lc, O + La, O + Lb, O + Lb + Lc, O + La + Lc, O + La + Lb] end = np.r_[O + La, O + Lb, O + Lc, O + Lb + La, O + Lc + Lb, O + La + Lc, O + Lc + La, O + La + Lb, O + Lb + Lc, O + Lb + Lc + La, O + La + Lc + Lb, O + La + Lb + Lc] rgb = [0xFF0000, 0x00FF00, 0x0000FF]*4 mv.add_representation('lines', {'startCoords': start, 'endCoords': end, 'startColors': rgb, 'endColors': rgb}) return mv
def test_multiple_elements(self): a = molecule('HCOOH') a.center(vacuum=5.0) io.write('HCOOH.cfg', a) i = neighbour_list("i", a, 1.85) self.assertArrayAlmostEqual(np.bincount(i), [2,3,1,1,1]) cutoffs = {(1, 6): 1.2} i = neighbour_list("i", a, cutoffs) self.assertArrayAlmostEqual(np.bincount(i), [0,1,0,0,1]) cutoffs = {(6, 8): 1.4} i = neighbour_list("i", a, cutoffs) self.assertArrayAlmostEqual(np.bincount(i), [1,2,1]) cutoffs = {('H', 'C'): 1.2, (6, 8): 1.4} i = neighbour_list("i", a, cutoffs) self.assertArrayAlmostEqual(np.bincount(i), [1,3,1,0,1])
def test_three_angles(self): a = ase.Atoms('CCC', positions=[[0.5, 0.5, 0.5], [0.5, 0.5, 1.0], [0.5, 1.0, 1.0]], cell=[2, 2, 2], pbc=True) i, j, dr = neighbour_list("ijD", a, 1.1) hist = angle_distribution(i, j, dr, 20) # v 45 degrees self.assertArrayAlmostEqual(hist, [0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0])
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) f = self.f nat = len(self.atoms) if atoms.has("size"): size = self.atoms.get_array("size") else: raise AttributeError( "Attribute error: Unable to load atom sizes from atoms object!" ) i_n, j_n, dr_nc, abs_dr_n = neighbour_list( "ijDd", self.atoms, f.get_maxSize() * f.get_cutoff()) ijsize = f.mix_sizes(size[i_n], size[j_n]) # Mask neighbour list to consider only true neighbors mask = abs_dr_n <= f.get_cutoff() * ijsize i_n = i_n[mask] j_n = j_n[mask] dr_nc = dr_nc[mask] abs_dr_n = abs_dr_n[mask] ijsize = ijsize[mask] e_n = f(abs_dr_n, ijsize) de_n = f.first_derivative(abs_dr_n, ijsize) # Energy epot = 0.5 * np.sum(e_n) # Forces df_nc = 0.5 * de_n.reshape(-1, 1) * dr_nc / abs_dr_n.reshape(-1, 1) # Sum for each atom fx_i = np.bincount(i_n, weights=df_nc[:, 0], minlength=nat) - \ np.bincount(j_n, weights=df_nc[:, 0], minlength=nat) fy_i = np.bincount(i_n, weights=df_nc[:, 1], minlength=nat) - \ np.bincount(j_n, weights=df_nc[:, 1], minlength=nat) fz_i = np.bincount(i_n, weights=df_nc[:, 2], minlength=nat) - \ np.bincount(j_n, weights=df_nc[:, 2], minlength=nat) # Virial virial_v = -np.array([ dr_nc[:, 0] * df_nc[:, 0], # xx dr_nc[:, 1] * df_nc[:, 1], # yy dr_nc[:, 2] * df_nc[:, 2], # zz dr_nc[:, 1] * df_nc[:, 2], # yz dr_nc[:, 0] * df_nc[:, 2], # xz dr_nc[:, 0] * df_nc[:, 1] ]).sum(axis=1) # xy self.results = { 'energy': epot, 'stress': virial_v / self.atoms.get_volume(), 'forces': np.transpose([fx_i, fy_i, fz_i]) }
def test_direct_evaluation(self): a = FaceCenteredCubic('Au', size=[2,2,2]) a.rattle(0.1) calc = EAM('Au-Grochola-JCP05.eam.alloy') a.set_calculator(calc) f = a.get_forces() calc2 = EAM('Au-Grochola-JCP05.eam.alloy') i_n, j_n, dr_nc, abs_dr_n = neighbour_list('ijDd', a, cutoff=calc2.cutoff) epot, virial, f2 = calc2.energy_virial_and_forces(a.numbers, i_n, j_n, dr_nc, abs_dr_n) self.assertArrayAlmostEqual(f, f2) a = FaceCenteredCubic('Cu', size=[2,2,2]) calc = EAM('CuAg.eam.alloy') a.set_calculator(calc) FIRE(StrainFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e_Cu = a.get_potential_energy()/len(a) a = FaceCenteredCubic('Ag', size=[2,2,2]) a.set_calculator(calc) FIRE(StrainFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e_Ag = a.get_potential_energy()/len(a) self.assertTrue(abs(e_Ag+2.85)<1e-6) a = L1_2(['Ag', 'Cu'], size=[2,2,2], latticeconstant=4.0) a.set_calculator(calc) FIRE(UnitCellFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e = a.get_potential_energy() syms = np.array(a.get_chemical_symbols()) self.assertTrue(abs((e-(syms=='Cu').sum()*e_Cu- (syms=='Ag').sum()*e_Ag)/len(a)-0.096)<0.0005) a = B1(['Ag', 'Cu'], size=[2,2,2], latticeconstant=4.0) a.set_calculator(calc) FIRE(UnitCellFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e = a.get_potential_energy() syms = np.array(a.get_chemical_symbols()) self.assertTrue(abs((e-(syms=='Cu').sum()*e_Cu- (syms=='Ag').sum()*e_Ag)/len(a)-0.516)<0.0005) a = B2(['Ag', 'Cu'], size=[2,2,2], latticeconstant=4.0) a.set_calculator(calc) FIRE(UnitCellFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e = a.get_potential_energy() syms = np.array(a.get_chemical_symbols()) self.assertTrue(abs((e-(syms=='Cu').sum()*e_Cu- (syms=='Ag').sum()*e_Ag)/len(a)-0.177)<0.0003) a = L1_2(['Cu', 'Ag'], size=[2,2,2], latticeconstant=4.0) a.set_calculator(calc) FIRE(UnitCellFilter(a, mask=[1,1,1,0,0,0]), logfile=None).run(fmax=0.001) e = a.get_potential_energy() syms = np.array(a.get_chemical_symbols()) self.assertTrue(abs((e-(syms=='Cu').sum()*e_Cu- (syms=='Ag').sum()*e_Ag)/len(a)-0.083)<0.0005)
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) nat = len(self.atoms) atnums = self.atoms.numbers atnums_in_system = set(atnums) i_n, j_n, dr_nc, abs_dr_n = neighbour_list( 'ijDd', self.atoms, self.dict) e_n = np.zeros_like(abs_dr_n) de_n = np.zeros_like(abs_dr_n) for params, pair in enumerate(self.dict): if pair[0] == pair[1]: mask1 = atnums[i_n] == pair[0] mask2 = atnums[j_n] == pair[0] mask = np.logical_and(mask1, mask2) e_n[mask] = self.f[pair](abs_dr_n[mask]) de_n[mask] = self.df[pair](abs_dr_n[mask]) if pair[0] != pair[1]: mask1 = np.logical_and( atnums[i_n] == pair[0], atnums[j_n] == pair[1]) mask2 = np.logical_and( atnums[i_n] == pair[1], atnums[j_n] == pair[0]) mask = np.logical_or(mask1, mask2) e_n[mask] = self.f[pair](abs_dr_n[mask]) de_n[mask] = self.df[pair](abs_dr_n[mask]) epot = 0.5*np.sum(e_n) # Forces df_nc = -0.5*de_n.reshape(-1, 1)*dr_nc/abs_dr_n.reshape(-1, 1) # Sum for each atom fx_i = np.bincount(j_n, weights=df_nc[:, 0], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:, 0], minlength=nat) fy_i = np.bincount(j_n, weights=df_nc[:, 1], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:, 1], minlength=nat) fz_i = np.bincount(j_n, weights=df_nc[:, 2], minlength=nat) - \ np.bincount(i_n, weights=df_nc[:, 2], minlength=nat) # Virial virial_v = -np.array([dr_nc[:, 0]*df_nc[:, 0], # xx dr_nc[:, 1]*df_nc[:, 1], # yy dr_nc[:, 2]*df_nc[:, 2], # zz dr_nc[:, 1]*df_nc[:, 2], # yz dr_nc[:, 0]*df_nc[:, 2], # xz dr_nc[:, 0]*df_nc[:, 1]]).sum(axis=1) # xy self.results = {'energy': epot, 'stress': virial_v/self.atoms.get_volume(), 'forces': np.transpose([fx_i, fy_i, fz_i])}
def find_tip_broken_bonds(atoms, cutoff, bulk_nn=4, boundary_thickness=None): """ Find position of the tip from the atom coordination, i.e. broken bonds. Using the C implementation of 'neighbour_list'. Returns the tip's position in cartesian coordinates. Parameters ---------- atoms : ase.Atoms Atomic configuration. cutoff : float Cutoff distance for neighbour search. bulk_nn : integer Number of nearest neighbours for the standard bulk configuration. boundary_buffer : float Thickness of the boundaries. Defaults to cutoff distance. Returns ------- tip_position : numpy array The x and y values are found. The z value is calculated as the midpoint of the depth. """ # initialisation of the boundaries if boundary_thickness is None: boundary_thickness = cutoff right_boundary = atoms.positions[(np.argmax(atoms.positions[:,0], axis=0)), 0] - boundary_thickness top_boundary = atoms.positions[(np.argmax(atoms.positions[:,1], axis=0)), 1] - boundary_thickness bottom_boundary = atoms.positions[(np.argmin(atoms.positions[:,1], axis=0)), 1] + boundary_thickness left_boundary = atoms.positions[(np.argmin(atoms.positions[:,0], axis=0)), 0] + boundary_thickness # calculating the coordination from the neighbours list i = neighbour_list("i", atoms, cutoff) coordination_list = np.bincount(i, minlength=len(atoms)) # list of atom numbers with at least one broken bond broken_bonds_array = np.where(coordination_list <= bulk_nn-1) # finds the atom number with the most positive x-valued position with a broken bond(s) # within the bounded section atom_number = 0 for m in range(0, len(broken_bonds_array[0])): temp_atom_pos = atoms.positions[broken_bonds_array[0][m]] if temp_atom_pos[0] > atoms.positions[atom_number,0]: if left_boundary < temp_atom_pos[0] < right_boundary: if bottom_boundary < temp_atom_pos[1] < top_boundary: atom_number = m tip_position = atoms.positions[broken_bonds_array[0][atom_number]] return np.array((tip_position[0], tip_position[1], atoms.cell[2,2]/2.0))
def internal_vector(atoms, at_idx, exponent, r_cut, do_calc_connect=True): iv = sp.zeros(3) atom, r = neighbour_list("iD", atoms, 8.0) r = np.asarray(r)[atom == at_idx] # if do_calc_connect: # atoms.set_cutoff(8.0) # atoms.calc_connect() # # each copy of each atom within the cutoff radius contribute to iv # r = np.asarray([neighbours.diff for neighbours in atoms.neighbours[at_idx]]) r_mag = np.asarray(map(LA.norm, r)) return (r / r_mag[:,None] * sp.exp(- (r_mag / r_cut) ** exponent)[:,None]).sum(axis=0)
def calc_rdf(Cell, rdf_cutoff, rdf_nbins): # use ase object for calculating rdf. r = neighbour_list('d', Cell, cutoff=rdf_cutoff) rdf, bin_edges = np.histogram(r, bins=rdf_nbins, range=(0, rdf_cutoff)) # normalize by bin volume and total number of atoms rdf = rdf / (len(Cell) * 4 * np.pi / 3 * (bin_edges[1:]**3 - bin_edges[:-1]**3)) # normalize by cell volume rdf /= len(Cell) / Cell.get_volume() bin_centers = (bin_edges[1:] + bin_edges[:-1]) / 2 return bin_centers, rdf
def test_small_cell(self): a = ase.Atoms('C', positions=[[0.5, 0.5, 0.5]], cell=[1, 1, 1], pbc=True) i, j, dr, shift = neighbour_list("ijDS", a, 1.1) assert np.bincount(i)[0] == 6 assert (dr == shift).all() i, j = neighbour_list("ij", a, 1.5) assert np.bincount(i)[0] == 18 a.set_pbc(False) i = neighbour_list("i", a, 1.1) assert len(i) == 0 a.set_pbc([True, False, False]) i = neighbour_list("i", a, 1.1) assert np.bincount(i)[0] == 2 a.set_pbc([True, False, True]) i = neighbour_list("i", a, 1.1) assert np.bincount(i)[0] == 4
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) # construct neighbor list i_p, j_p, r_p, r_pc = neighbour_list('ijdD', atoms=atoms, cutoff=self.cutoff) nb_atoms = len(self.atoms) nb_pairs = len(i_p) # normal vectors n_pc = (r_pc.T / r_p).T nx_p, ny_p, nz_p = n_pc.T # construct triplet list first_i = first_neighbours(nb_atoms, i_p) ij_t, ik_t = triplet_list(first_i) # calculate energy G_t = self.G(r_pc[ij_t], r_pc[ik_t]) xi_p = np.bincount(ij_t, weights=G_t, minlength=nb_pairs) F_p = self.F(r_p, xi_p) epot = 0.5 * np.sum(F_p) d1G_t = self.d1G(r_pc[ij_t], r_pc[ik_t]) d2F_d2G_t = (self.d2F(r_p[ij_t], xi_p[ij_t]) * self.d2G(r_pc[ij_t], r_pc[ik_t]).T).T # calculate forces (per pair) fx_p = \ self.d1F(r_p, xi_p) * n_pc[:, 0] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 0], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 0], minlength=nb_pairs) fy_p = \ self.d1F(r_p, xi_p) * n_pc[:, 1] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 1], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 1], minlength=nb_pairs) fz_p = \ self.d1F(r_p, xi_p) * n_pc[:, 2] + \ self.d2F(r_p, xi_p) * np.bincount(ij_t, d1G_t[:, 2], minlength=nb_pairs) + \ np.bincount(ik_t, d2F_d2G_t[:, 2], minlength=nb_pairs) # collect atomic forces fx_n = 0.5 * (np.bincount(i_p, weights=fx_p) - np.bincount(j_p, weights=fx_p)) fy_n = 0.5 * (np.bincount(i_p, weights=fy_p) - np.bincount(j_p, weights=fy_p)) fz_n = 0.5 * (np.bincount(i_p, weights=fz_p) - np.bincount(j_p, weights=fz_p)) f_n = np.transpose([fx_n, fy_n, fz_n]) self.results = {'energy': epot, 'forces': f_n}
def test_out_of_bounds(self): nat = 10 atoms = ase.Atoms(numbers=range(nat), cell=[(0.2, 1.2, 1.4), (1.4, 0.1, 1.6), (1.3, 2.0, -0.1)]) atoms.set_scaled_positions(3 * np.random.random((nat, 3)) - 1) for p1 in range(2): for p2 in range(2): for p3 in range(2): atoms.set_pbc((p1, p2, p3)) i, j, d, D, S = neighbour_list("ijdDS", atoms, atoms.numbers * 0.2 + 0.5) c = np.bincount(i) atoms2 = atoms.repeat((p1 + 1, p2 + 1, p3 + 1)) i2, j2, d2, D2, S2 = neighbour_list("ijdDS", atoms2, atoms2.numbers * 0.2 + 0.5) c2 = np.bincount(i2) c2.shape = (-1, nat) dd = d.sum() * (p1 + 1) * (p2 + 1) * (p3 + 1) - d2.sum() dr = np.linalg.solve(atoms.cell.T, (atoms.positions[1]-atoms.positions[0]).T).T+np.array([0,0,3]) self.assertTrue(abs(dd) < 1e-10) self.assertTrue(not (c2 - c).any())
def test_wrong_number_of_cutoffs(self): nat = 10 atoms = ase.Atoms(numbers=range(nat), cell=[(0.2, 1.2, 1.4), (1.4, 0.1, 1.6), (1.3, 2.0, -0.1)]) atoms.set_scaled_positions(3 * np.random.random((nat, 3)) - 1) exception_thrown = False try: i, j, d, D, S = neighbour_list("ijdDS", atoms, np.ones(len(atoms)-1)) except TypeError: exception_thrown = True self.assertTrue(exception_thrown)
def test_wrong_number_of_cutoffs(self): nat = 10 atoms = ase.Atoms(numbers=range(nat), cell=[(0.2, 1.2, 1.4), (1.4, 0.1, 1.6), (1.3, 2.0, -0.1)]) atoms.set_scaled_positions(3 * np.random.random((nat, 3)) - 1) exception_thrown = False try: i, j, d, D, S = neighbour_list("ijdDS", atoms, np.ones(len(atoms) - 1)) except TypeError: exception_thrown = True self.assertTrue(exception_thrown)
def test_dsygv_dgelsd(self): a = Diamond('C', size=[4,4,4]) b = a.copy() b.positions += (np.random.random(b.positions.shape)-0.5)*0.1 i, j = neighbour_list("ij", b, 1.85) dr_now = mic(b.positions[i] - b.positions[j], b.cell) dr_old = mic(a.positions[i] - a.positions[j], a.cell) dgrad1 = get_delta_plus_epsilon_dgesv(len(b), i, dr_now, dr_old) dgrad2 = get_delta_plus_epsilon(len(b), i, dr_now, dr_old) self.assertArrayAlmostEqual(dgrad1, dgrad2)
def find_bonds(atoms, cutoffs_dict): """Returns a set of (frozen)sets of indices, where each set represents a bond, and the two values in the set represent the indices of the atoms in the system. ARGUMENTS --------- atoms : <ase.atoms.Atoms> object cutoffs_dict : <dict> of cutoffs with the form { (species tuple) : cutoff } Example: -------- cutoffs_dict = { ('C', 'C') : 1.8, ('C', 'H') : 1.3, ('H', 'H') : 0.9 } COMMENTS -------- - Matscipy is used to generate the neighbour lists, which are determined by simple cutoffs. The function assumes that two atoms form a `bond' if they are within the cutoff length corresponding to that bond type. - By returning a `set', it is ensured that there are no redundant bonds, since {a, b} == {b, a}. - This function can also be used to find bond events by comparing the bond sets from consecutive simulation steps and taking the symmetric difference between the two. Note that the order *does* matter! For example: >>> bonds_initial = find_bonds( atoms_initial, cutoffs ) >>> bonds_final = find_bonds( atoms_final, cutoffs ) >>> >>> bonds_final - bonds_initial {frozenset({74, 29}) frozenset({104, 19})} >>> # two bonds have been created >>> >>> bonds_initial - bonds_final {frozenset({31, 78})} >>> # one bond has been broken """ i, j = neighbour_list('ij', atoms, cutoffs_dict) return set(frozenset({x, y}) for x, y in zip(i, j))
def complete_Si_tetrahedrons(atoms, cutoff=2.0): """ Every 3-coordinated Si atom gets an extra O atom to complete the tetrahedron. """ species = atoms.get_atomic_numbers() ii, jj, DD, SS = neighbour_list("ijDS", atoms, cutoff) for i in range(len(atoms)): if species[i] == 14: neighbours = np.where(ii == i)[0] distances = DD[neighbours] if len(neighbours) == 3: pos4thO = tetrahedron_4th_position(np.zeros(3), distances) atoms.append(Atom(symbol="O", position=pos4thO + atoms.get_positions()[i])) return
def find_crack_tip(atoms, dt=None, store=True, results=None): """ Return atom at the crack tip and its x-coordinate Crack tip is defined to be location of rightmost atom whose nearest neighbour is at distance > 2.5*a """ calc = atoms.get_calculator() a = calc.parameters['a'] rc = calc.parameters['rc'] i = neighbour_list('i', atoms, rc) nn = np.bincount(i) # number of nearest neighbours, equal to 6 in bulk x = atoms.positions[:, 0] y = atoms.positions[:, 1] bottom = y.min() left = x.min() width = x.max() - x.min() height = y.max() - y.min() old_tip_x = atoms.info.get('tip_x', left + 0.3*width) # crack cannot have advanced more than c_R*dt if dt is not None: cl, ct, cR = calc.get_wave_speeds(atoms) tip_max_x = old_tip_x + 10.0*cR*dt # FIXME definition of cR seems wrong, shouldn't need factor of 10 here... else: tip_max_x = left + 0.8*width broken = ((nn != 6) & (x > left + 0.2*width) & (x < tip_max_x) & (y > bottom + 0.1*height) & (y < bottom + 0.9*height)) index = atoms.positions[broken, 0].argmax() tip_atom = broken.nonzero()[0][index] tip_x = atoms.positions[tip_atom, 0] strain = get_strain(atoms) eps_G = atoms.info['eps_G'] print 'tip_x: %.3f strain: %.4f delta: %.3f' % (tip_x, strain, strain/eps_G) if store: atoms.info['tip_atom'] = tip_atom atoms.info['tip_x'] = tip_x if results is not None: results.append(tip_x) return (tip_atom, tip_x, broken)
def find_crack_tip(atoms, dt=None, store=True, results=None): """ Return atom at the crack tip and its x-coordinate Crack tip is defined to be location of rightmost atom whose nearest neighbour is at distance > 2.5*a """ calc = atoms.get_calculator() a = calc.parameters['a'] rc = calc.parameters['rc'] i = neighbour_list('i', atoms, rc) nn = np.bincount(i) # number of nearest neighbours, equal to 6 in bulk x = atoms.positions[:, 0] y = atoms.positions[:, 1] bottom = y.min() left = x.min() width = x.max() - x.min() height = y.max() - y.min() old_tip_x = atoms.info.get('tip_x', left + 0.3 * width) # crack cannot have advanced more than c_R*dt if dt is not None: cl, ct, cR = calc.get_wave_speeds(atoms) tip_max_x = old_tip_x + 10.0 * cR * dt # FIXME definition of cR seems wrong, shouldn't need factor of 10 here... else: tip_max_x = left + 0.8 * width broken = ((nn != 6) & (x > left + 0.2 * width) & (x < tip_max_x) & (y > bottom + 0.1 * height) & (y < bottom + 0.9 * height)) index = atoms.positions[broken, 0].argmax() tip_atom = broken.nonzero()[0][index] tip_x = atoms.positions[tip_atom, 0] strain = get_strain(atoms) eps_G = atoms.info['eps_G'] print('tip_x: %.3f strain: %.4f delta: %.3f' % (tip_x, strain, strain / eps_G)) if store: atoms.info['tip_atom'] = tip_atom atoms.info['tip_x'] = tip_x if results is not None: results.append(tip_x) return (tip_atom, tip_x, broken)
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) i_n, j_n, dr_nc, abs_dr_n = neighbour_list('ijDd', self.atoms, self._db_cutoff) epot, virial_v, forces_ic = self.energy_virial_and_forces( self.atoms.numbers, i_n, j_n, dr_nc, abs_dr_n) self.results = { 'energy': epot, 'free_energy': epot, 'stress': virial_v / self.atoms.get_volume(), 'forces': forces_ic }
def calculate(self, atoms=None, properties=None, system_changes=all_changes): if properties is None: properties = ['energy', 'local_energy'] Calculator.calculate(self, atoms, properties, system_changes) if atoms is None: atoms = self.atoms natoms = len(atoms) D = self.parameters.D alpha = self.parameters.alpha r0 = self.parameters.r0 rc = self.parameters.rc i, j, dr, r = neighbour_list('ijDd', atoms, rc) tmp = np.exp(-alpha * (r - r0)) de = 0.5 * D * (tmp * tmp - 2.0 * tmp) local_energy = np.bincount(i, de, minlength=natoms) energy = local_energy.sum() tmpd = -alpha * tmp df = (D * (2.0 * tmp * tmpd - 2.0 * tmpd) / r)[:, np.newaxis] * dr forces = np.zeros((natoms, 3)) for kk in range(3): forces[:, kk] = np.bincount(i, weights=df[:, kk], minlength=natoms) if 'stress' in properties: if self.atoms.number_of_lattice_vectors == 3: virial = np.zeros(6) if len(i) > 0: virial = 0.5 * np.array([ dr[:, 0] * df[:, 0], # xx dr[:, 1] * df[:, 1], # yy dr[:, 2] * df[:, 2], # zz dr[:, 1] * df[:, 2], # yz dr[:, 0] * df[:, 2], # xz dr[:, 0] * df[:, 1] ]).sum(axis=1) # xy self.results['stress'] = virial / atoms.get_volume() else: raise PropertyNotImplementedError self.results['energy'] = energy self.results['free_energy'] = energy self.results['forces'] = forces self.results['local_energy'] = local_energy
def test_neighbour_list(self): a = io.read('aC.cfg') j, dr, i, abs_dr = neighbour_list("jDid", a, 1.85) self.assertTrue((np.bincount(i) == np.bincount(j)).all()) r = a.get_positions() dr_direct = mic(r[j] - r[i], a.cell) abs_dr_from_dr = np.sqrt(np.sum(dr * dr, axis=1)) abs_dr_direct = np.sqrt(np.sum(dr_direct * dr_direct, axis=1)) self.assertTrue(np.all(np.abs(abs_dr - abs_dr_from_dr) < 1e-12)) self.assertTrue(np.all(np.abs(abs_dr - abs_dr_direct) < 1e-12)) self.assertTrue(np.all(np.abs(dr - dr_direct) < 1e-12))
def test_neighbour_list(self): a = io.read('aC.cfg') j, dr, i, abs_dr = neighbour_list("jDid", a, 1.85) self.assertTrue((np.bincount(i) == np.bincount(j)).all()) r = a.get_positions() dr_direct = mic(r[j]-r[i], a.cell) abs_dr_from_dr = np.sqrt(np.sum(dr*dr, axis=1)) abs_dr_direct = np.sqrt(np.sum(dr_direct*dr_direct, axis=1)) self.assertTrue(np.all(np.abs(abs_dr-abs_dr_from_dr) < 1e-12)) self.assertTrue(np.all(np.abs(abs_dr-abs_dr_direct) < 1e-12)) self.assertTrue(np.all(np.abs(dr-dr_direct) < 1e-12))
def test_shrink_wrapped_direct_call(self): a = io.read('aC.cfg') r = a.positions j, dr, i, abs_dr, shift = neighbour_list("jDidS", positions=r, cutoff=1.85) self.assertTrue((np.bincount(i) == np.bincount(j)).all()) dr_direct = r[j] - r[i] abs_dr_from_dr = np.sqrt(np.sum(dr * dr, axis=1)) abs_dr_direct = np.sqrt(np.sum(dr_direct * dr_direct, axis=1)) self.assertTrue(np.all(np.abs(abs_dr - abs_dr_from_dr) < 1e-12)) self.assertTrue(np.all(np.abs(abs_dr - abs_dr_direct) < 1e-12)) self.assertTrue(np.all(np.abs(dr - dr_direct) < 1e-12))
def test_neighbour_list(self): for pbc in [True, False, [True, False, True]]: a = io.read('aC.cfg') a.set_pbc(pbc) j, dr, i, abs_dr, shift = neighbour_list("jDidS", a, 1.85) self.assertTrue((np.bincount(i) == np.bincount(j)).all()) r = a.get_positions() dr_direct = mic(r[j]-r[i], a.cell) self.assertArrayAlmostEqual(r[j]-r[i]+shift.dot(a.cell), dr_direct) abs_dr_from_dr = np.sqrt(np.sum(dr*dr, axis=1)) abs_dr_direct = np.sqrt(np.sum(dr_direct*dr_direct, axis=1)) self.assertTrue(np.all(np.abs(abs_dr-abs_dr_from_dr) < 1e-12)) self.assertTrue(np.all(np.abs(abs_dr-abs_dr_direct) < 1e-12)) self.assertTrue(np.all(np.abs(dr-dr_direct) < 1e-12))
def get_angle_distribution(at, cutoff=1.98): from matscipy.neighbours import neighbour_list from itertools import combinations ii, jj, DD = neighbour_list('ijD', at, cutoff) angles = [] for atom in at: if atom.number == 8: neighs = np.where(ii == atom.index)[0] # Si_1, Si_2 = jj[neighs] # D_1, D_2 = DD[neighs] # print(np.linalg.norm(D_2 - D_1)) # print(Si_1, Si_2) for Si_1, Si_2 in combinations(jj[neighs], 2): angle = at.get_angle([Si_1, atom.index, Si_2]) angles.append(angle) return np.array(angles)
def atomic_strain(atoms_now, atoms_old, cutoff=None, neighbours=None): """ Calculate deformation gradient tensor and D^2_min measure for non-affine displacements. See: Falk, Langer, Phys. Rev. B 57, 7192 (1998) Parameters: ----------- atoms_now : ase.Atoms Current atomic configuration atoms_old : ase.Atoms Reference atomic configuration cutoff : float Neighbor list cutoff. neighbours : ( array_like, array_like ) Neighbor list. Automatically computed if not provided. Returns: -------- delta_plus_epsilon : array 3x3 deformation gradient tensor for each atom. residual : array D^2_min norm for each atom """ if neighbours is None: if cutoff is None: raise ValueError('Please provide either neighbor list or neighbor ' 'list cutoff.') # Get neighbours i_now, j_now = neighbour_list("ij", atoms_now, cutoff) elif cutoff is not None: raise ValueError('Please provide either neighbor list or neighbor ' 'list cutoff, not both.') else: i_now, j_now = neighbours # Get strain gradient tensor and D square values delta_plus_epsilon, residual = get_D_square_min(atoms_now, atoms_old, i_now, j_now) return delta_plus_epsilon, residual
def test_single_ring(self): a = ase.build.molecule('C6H6') a = a[a.numbers==6] a.center(vacuum=5) i, j, r = neighbour_list('ijD', a, 1.85) d = distances_on_graph(i, j) self.assertEqual(d.shape, (6,6)) self.assertArrayAlmostEqual(d-d.T, np.zeros_like(d)) dcheck = np.arange(len(a)) dcheck = np.abs(dcheck.reshape(1,-1)-dcheck.reshape(-1,1)) dcheck = np.where(dcheck > len(a)/2, len(a)-dcheck, dcheck) self.assertArrayAlmostEqual(d, dcheck) r = find_sp_rings(i, j, r, d) self.assertArrayAlmostEqual(r, [0,0,0,0,0,0,1])
def hydrogenate_Os(atoms, OHdistance=0.98, cutoff=2.0, mask=None): """ Every Oxygen atom that sticks out undercoordinated gets a H atom. Orientation of O--H bond is rather arbitrary. """ if mask is None: mask = np.ones(len(atoms)) ii, jj, DD, SS = neighbour_list("ijDS", atoms, cutoff) species = atoms.get_atomic_numbers() for i, yes in zip(range(len(atoms)), mask): if species[i] == 8 and yes: neighbours = np.where(ii == i)[0] if len(neighbours) == 1: distance = DD[neighbours].flatten() posH = atoms.get_positions()[i] - OHdistance * distance / LA.norm(distance) atoms.append(Atom(symbol="H", position=posH)) return
def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms, properties, system_changes) a = self.parameters['a'] rc = self.parameters['rc'] k = self.parameters['k'] beta = self.parameters['beta'] energies = np.zeros(len(atoms)) forces = np.zeros((len(atoms), 3)) velocities = (atoms.get_momenta().T/atoms.get_masses()).T i, j, dr, r = neighbour_list('ijDd', atoms, rc) if len(i) > 0: dr_hat = (dr.T/r).T dv = velocities[j] - velocities[i] de = 0.5*k*(r - a)**2 # spring energies e = 0.5*de # half goes to each end of spring f = (k*(r - a)*dr_hat.T).T + beta*dv energies[:] = np.bincount(i, e) for kk in range(3): forces[:, kk] = np.bincount(i, weights=f[:, kk]) energy = energies.sum() # add energy 0.5*k*(rc - a)**2 for each broken bond if len(i) < self.crystal_bonds: de = 0.5*k*(rc - a)**2 energy += 0.5*de*(self.crystal_bonds - len(i)) # Stokes dissipation if 'stokes' in atoms.arrays: b = atoms.get_array('stokes') forces -= (velocities.T*b).T self.results = {'energy': energy, 'forces': forces}
def spatial_correlation_function_near(atoms, values, gridsize=None, cutoff=None, norm=False): if gridsize is None: gridsize = 0.1 if cutoff is None: cutoff = 7.5 # close range exact calculation nbins = int(cutoff/gridsize)+1 index1,index2,dist = neighbour_list('ijd', atoms, cutoff=cutoff) SCF_near, edges = np.histogram(dist, bins=bins, weights=values[index1] *values[index2]) slice_volume = 4*np.pi/3 * (edges[1:]**3-edges[:-1]**3) SCF_near *= atoms.get_volume()/n_atoms**2 / slice_volume if norm: v_2_mean = (values**2).mean() v_mean_2 = (values.mean())**2 SCF_near = (SCF_near-v_mean_2)/(v_2_mean-v_mean_2) return SCF_near, (edges[1:]+edges[:-1])/2
def ring_statistics(a, cutoff, maxlength=-1): """ Compute number of shortest path rings in sample. See: D.S. Franzblau, Phys. Rev. B 44, 4925 (1991) Parameters ---------- a : ase.Atoms Atomic configuration. cutoff : float Cutoff for neighbor counting. maxlength : float, optional Maximum ring length. Search for rings will stop at this length. This is useful to speed up calculations for large systems. Returns ------- ringstat : array Array with number of shortest path rings. """ i, j, r = neighbour_list('ijD', a, cutoff) d = _matscipy.distances_on_graph(i, j) return _matscipy.find_sp_rings(i, j, r, d, maxlength)
from fundef import minimal_cycles from itertools import combinations import _matscipy ######################################################################## ######################################################################## at = aseread('../data/bilayer_TSmin.xyz', format='extxyz') top = at[np.where(at.positions[:,2] > -.5)[0]] top_si = top[np.where(top.get_atomic_numbers() == 14)[0]] top_o = top[np.where(top.get_atomic_numbers() == 8)[0]] cutoff = 1.97 ii, jj = neighbour_list('ij', top, cutoff) neighbour_si = [] neighbour_o = [] angles = [] graph = nx.Graph() for atom in top: if atom.number == 8: neighs = np.where(ii == atom.index)[0] for Si_1, Si_2 in combinations(jj[neighs], 2): graph.add_edge(Si_1, Si_2) graph[Si_1][Si_2]['oxygen'] = atom.index neighbour_o.append(atom.index) neighbour_si.append([Si_1, Si_2]) angle = at.get_angle([Si_1, atom.index, Si_2]) angles.append(angle)
# Center notched configuration in simulation cell and ensure enough vacuum. oldr = a[0].position.copy() a.center(vacuum=params.vacuum, axis=0) a.center(vacuum=params.vacuum, axis=1) tip_x += a[0].position[0] - oldr[0] tip_y += a[0].position[1] - oldr[1] # Choose which bond to break. bond1, bond2 = parameter('bond', crack.find_tip_coordination(a, bondlength=bondlength)) # Hydrogenate? if parameter('hydrogenate', False): # Get surface atoms of cluster with crack coord = np.bincount(neighbour_list('i', a, bondlength), minlength=len(a)) # Exclude all atoms of the crack face from hydrogenation exclude = np.logical_and(a.get_array('groups')==1, coord!=4) a.set_array('coord', coord) a.set_array('exclude', exclude) a = hydrogenate(cryst, bondlength, parameter('XH_bondlength'), b=a, exclude=exclude) g = a.get_array('groups') g[np.array(a.get_chemical_symbols())=='H'] = -1 a.set_array('groups', g) ase.io.write('{0}_hydrogenated.cfg'.format(basename), a) # Move reference crystal by same amount cryst.set_cell(a.cell) cryst.set_pbc([False, False, True]) cryst.translate(a[0].position - oldr)