def void_find(meshobject, atoms, coarseness=1): """ Defines the maximum spherical radius of probes in unoccupied space. Scans through the distance between the probe point and all atoms in the system, and defines the maximum radius of the probe as the smallest value of (probe_position - atom_position - vdw_radii). Coarseness factor reduces number of probe points by skipping over points in the grid. Args: meshobject: Mesh object Contains meshgrid and associated unit cell information. atoms: Atoms object Supplies coordinates and atomic information. coarseness: integer Skips over an integer number of points Return: void_centres: numpy array Atomic coordiantes of the void centres. void_radii: numpy array Radius of the void centres """ import numpy as np from ase.data.vdw_alvarez import vdw_radii from ase.geometry import get_distances from carmm.analyse.meshgrid.meshgrid_functions import mol_mesh_pbc_check # Check atoms PBC and mesh PBC match. if meshobject.strict_mode: mol_mesh_pbc_check(meshobject.pbc, atoms.pbc) void_centres = [] void_radii = [] atom_radii = vdw_radii[atoms.numbers] atom_radii = np.array([atom_radii]).flatten() for i in range(0, meshobject.nx, coarseness): for j in range(0, meshobject.ny, coarseness): for k in range(0, meshobject.nz, coarseness): a_xx, a_yy, a_zz = meshobject.xx[i, j, k], meshobject.yy[ i, j, k], meshobject.zz[i, j, k] distances = get_distances(np.array([a_xx, a_yy, a_zz]), p2=atoms.positions, cell=meshobject.Cell, pbc=meshobject.pbc) distances = distances[1] - atom_radii min_distance = np.amin(distances) void_centres.append([a_xx, a_yy, a_zz]) void_radii.append([min_distance]) void_centres = np.array([void_centres]) void_centres = void_centres[0] void_radii = np.array([void_radii]).flatten() return void_centres, void_radii
def get_geometry(atoms, index, code): from ase.geometry import get_distances from collections import defaultdict #get possible rings, and max rings size ring_sizes = get_ring_sizes(code) * 2 max_ring = max(ring_sizes) # repeat the unit cell so it is large enough to capture the max ring size # also turn this new larger unit cell into a graph G, large_atoms, repeat = atoms_to_graph(atoms, index, max_ring) index = [atom.index for atom in large_atoms if atom.tag == index][0] # to find all the rings associated with a T site, we need all the rings # associated with each oxygen bound to that T site. We will use networkx # neighbors to find those oxygens atoms = atoms.repeat(repeat) import networkx as nx com_dists = defaultdict(list) for n in nx.neighbors(G, index): c, paths, a = get_orings(large_atoms, n, code, validation='sastre') atoms, trans = center(atoms, large_atoms[n].tag) for p in paths: l = int(len(p) / 2) ring_atoms = atoms[p] rp = ring_atoms.get_positions() com = rp.mean(axis=0) positions = atoms.get_positions() distances = get_distances(com, positions)[1][0] for i, d in enumerate(distances): if i not in p: com_dists[l].append(d) return com_dists
def nearest(atoms1, atoms2, cell=None, pbc=None): """Return indices of nearest atoms""" p1 = atoms1.get_positions() p2 = atoms2.get_positions() vd_aac, d2_aa = get_distances(p1, p2, cell, pbc) i1, i2 = np.argwhere(d2_aa == d2_aa.min())[0] return i1, i2, vd_aac[i1, i2]
def scale_cell(atoms): from ase.geometry import get_distances diff = 1 sil = 3.1 mult = 1 si = [atom.index for atom in atoms if atom.symbol == 'Si'] zsi = atoms[si] while diff > 0.01: cell = atoms.cell.cellpar() for i in range(3): cell[i] = cell[i] * mult zsi.set_cell(cell, scale_atoms=True) ncell = zsi.get_cell() positions = zsi.get_positions() distances = get_distances(positions, cell=ncell, pbc=[1, 1, 1])[1] temp = [] for line in distances: masked_line = np.ma.masked_equal(line, 0.0, copy=False) temp.append(masked_line.min()) silm = np.average(temp) diff = abs(silm - sil) mult = sil / silm atoms.set_cell(cell, scale_atoms=True) return atoms
def get_qm_cluster(self, atoms): if self.qm_buffer_mask is None: self.initialize_qm_buffer_mask(atoms) qm_cluster = atoms[self.qm_buffer_mask] del qm_cluster.constraints round_cell = False if self.qm_radius is None: round_cell = True # get all distances between qm atoms. # Treat all X, Y and Z directions independently # only distance between qm atoms is calculated # in order to estimate qm radius in thee directions R_qm, _ = get_distances(atoms.positions[self.qm_selection_mask], cell=atoms.cell, pbc=atoms.pbc) # estimate qm radius in three directions as 1/2 # of max distance between qm atoms self.qm_radius = np.amax(np.amax(R_qm, axis=1), axis=0) * 0.5 if atoms.cell.orthorhombic: cell_size = np.diagonal(atoms.cell) else: raise RuntimeError("NON-orthorhombic cell is not supported!") # check if qm_cluster should be left periodic # in periodic directions of the cell (cell[i] < qm_radius + buffer # otherwise change to non pbc # and make a cluster in a vacuum configuration qm_cluster_pbc = (atoms.pbc & (cell_size < 2.0 * (self.qm_radius + self.buffer_width))) # start with the original orthorhombic cell qm_cluster_cell = cell_size.copy() # create a cluster in a vacuum cell in non periodic directions qm_cluster_cell[~qm_cluster_pbc] = (2.0 * (self.qm_radius[~qm_cluster_pbc] + self.buffer_width + self.vacuum)) if round_cell: # round the qm cell to the required tolerance qm_cluster_cell[~qm_cluster_pbc] = (np.round( (qm_cluster_cell[~qm_cluster_pbc]) / self.qm_cell_round_off) * self.qm_cell_round_off) qm_cluster.set_cell(Cell(np.diag(qm_cluster_cell))) qm_cluster.pbc = qm_cluster_pbc qm_shift = (0.5 * qm_cluster.cell.diagonal() - qm_cluster.positions.mean(axis=0)) if 'cell_origin' in qm_cluster.info: del qm_cluster.info['cell_origin'] # center the cluster only in non pbc directions qm_cluster.positions[:, ~qm_cluster_pbc] += qm_shift[~qm_cluster_pbc] return qm_cluster
def gofr_snapshot(axes, pos, rmax, rmin=0, nbin=40, gofr_norm=None): """ calculate the pair correlation function of a snapshot of a crystal structure given the 'axes' of the simulation cell and the positions ('pos') of atoms inside the cell. 'rmax' is the maximum pair distance to histogram. 'rmin','rmax', and 'nbin' are passed to numpy.histogram to generate the results. return 3 lists: r, g(r), and g(r) normalization. Note: if a gofr_norm is given, then it will not be recalculated. This can save time if the simulation box does not change. Args: axes (np.array): crystal lattice vectors. pos (np.array): positions of atoms. rmax (float): maximum distance to calculate g(r) to. rmin (float,optional): minimum distance to calculate g(r) to, default is 0. nbin (int,optional): number of bins, default is 40. gofr_norm (float,optional): normalization factor for g(r), default is to recalculate. Returns: (np.array,np.array,float): (r,g(r),normalization) """ from ase.geometry import get_distances from qharv.inspect.axes_pos import volume cell_volume = volume(axes) nptcl = len(pos) dtable = get_distances(pos, cell=axes, pbc=True)[1] # upper triagular distance matrix (i.e. unique pair distances) i_triu = np.triu_indices(nptcl, m=nptcl, k=1) pair_dists = dtable[i_triu] counts, ticks = np.histogram(pair_dists, range=(rmin, rmax), bins=nbin) myx = (ticks[:-1] + ticks[1:]) / 2. dx = myx[1] - myx[0] if gofr_norm is None: # gofr_norm = 4*np.pi*myx**2.*dx * nptcl*(nptcl-1)/2./struct.volume gofr_norm = 4 * np.pi * myx**2. * dx * nptcl**2. / cell_volume / 2. # end if myy = counts / gofr_norm return myx, myy, gofr_norm
def distance_point2point(x_1, y_1, z_1, x_2, y_2, z_2, meshobject): """ Function finds the distance between two points (defined in cartesian co-ordinates). Args: x_1: float x coordinate of point 1. y_1: float y coordinate of point 1. z_1: float z coordinate of point 1. x_2: float x coordinate of point 2. y_2: float y coordinate of point 2. z_2: float2 z coordinate of point 2. meshobject: Mesh object Object storing meshgrid and PBC conditions Returns: o_distance: Distance between points 1 and 2. """ from ase.geometry import get_distances o_distance = get_distances([x_1, y_1, z_1], p2=[x_2, y_2, z_2], cell=meshobject.Cell, pbc=meshobject.pbc) return o_distance
def add(self): newatoms = self.get_atoms() if newatoms is None: # Error dialog was shown return newcenter = self.getcoords() # Not newatoms.center() because we want the same centering method # used for adding atoms relative to selections (mean). previous_center = newatoms.positions.mean(0) newatoms.positions += newcenter - previous_center atoms = self.gui.atoms if len(atoms) and self.picky.value: from ase.geometry import get_distances disps, dists = get_distances(atoms.positions, newatoms.positions) mindist = dists.min() if mindist < 0.5: ui.showerror( _('Bad positions'), _('Atom would be less than 0.5 Å from ' 'an existing atom. To override, ' 'uncheck the check positions option.')) return self.gui.add_atoms_and_select(newatoms)
def _isclose(self, molid1, molid2): name = f'{molid1}-{molid2}' if name not in self._distances: self._distances[name] = np.min( get_distances(self._atoms[self._mols[molid1 - 1]].positions, self._atoms[self._mols[molid2 - 1]].positions, cell=self._atoms.get_cell(), pbc=self._atoms.get_pbc())[1]) <= self.cutoff return self._distances[name]
def test_qm_buffer_mask(qm_calc, mm_calc, bulk_at): """ test number of atoms in qm_buffer_mask for spherical region in a fully periodic cell also tests that "region" array returns the same mapping """ alat = bulk_at.cell[0, 0] N_cell_geom = 10 at0 = bulk_at * N_cell_geom r = at0.get_distances(0, np.arange(len(at0)), mic=True) print("N_cell", N_cell_geom, 'N_MM', len(at0), "Size", N_cell_geom * alat) qm_rc = 5.37 # cutoff for EMC() for R_QM in [ 1.0e-3, # one atom in the center alat / np.sqrt(2.0) + 1.0e-3, # should give 12 nearest # neighbours + central atom alat + 1.0e-3 ]: # should give 18 neighbours + central atom at = at0.copy() qm_mask = r < R_QM qm_buffer_mask_ref = r < 2 * qm_rc + R_QM # exclude atoms that are too far (in case of non spherical region) # this is the old way to do it _, r_qm_buffer = get_distances(at.positions[qm_buffer_mask_ref], at.positions[qm_mask], at.cell, at.pbc) updated_qm_buffer_mask = np.ones_like(at[qm_buffer_mask_ref]) for i, r_qm in enumerate(r_qm_buffer): if r_qm.min() > 2 * qm_rc: updated_qm_buffer_mask[i] = False qm_buffer_mask_ref[qm_buffer_mask_ref] = updated_qm_buffer_mask ''' print(f'R_QM {R_QM} N_QM {qm_mask.sum()}') print(f'R_QM + buffer: {2 * qm_rc + R_QM:.2f}' f' N_QM_buffer {qm_buffer_mask_ref.sum()}') print(f' N_total: {len(at)}') ''' qmmm = ForceQMMM(at, qm_mask, qm_calc, mm_calc, buffer_width=2 * qm_rc) # build qm_buffer_mask and test it qmmm.initialize_qm_buffer_mask(at) # print(f' Calculator N_QM_buffer:' # f' {qmmm.qm_buffer_mask.sum().sum()}') assert qmmm.qm_buffer_mask.sum() == qm_buffer_mask_ref.sum() # same test for qmmm.get_cluster() qm_cluster = qmmm.get_qm_cluster(at) assert len(qm_cluster) == qm_buffer_mask_ref.sum() # test region mappings region = qmmm.get_region_from_masks(at) qm_mask_region = region == "QM" assert qm_mask_region.sum() == qm_mask.sum() buffer_mask_region = region == "buffer" assert qm_mask_region.sum() + \ buffer_mask_region.sum() == qm_buffer_mask_ref.sum()
def get_ads_dist(atoms, ads0, ads1='H'): index0 = [i for i in range(len(atoms)) if atoms[i].symbol == ads0] index1 = [i for i in range(len(atoms)) if atoms[i].symbol == ads1] dist = [] D, D_len = get_distances(atoms.positions[index0], atoms.positions[index1], atoms.cell, pbc=True) return np.max(D_len)
def slab_positions2ads_index(atoms, slab, species): """Return the indexes of adsorbate atoms identified by comparing positions to a reference slab structure. Parameters ---------- atoms : object """ composition = string2symbols(species) ads_atoms = [] for symbol in composition: if (composition.count(symbol) == atoms.get_chemical_symbols().count( symbol)): ads_atoms += [ atom.index for atom in atoms if atom.symbol == symbol ] ua_ads, uc_ads = np.unique(ads_atoms, return_counts=True) ua_comp, uc_comp = np.unique(composition, return_counts=True) if ua_ads == ua_comp and uc_ads == uc_comp: return ads_atoms p_a = atoms.get_positions() p_r = slab.get_positions() for s in composition: if s in np.array(atoms.get_chemical_symbols())[ads_atoms]: continue symbol_count = composition.count(s) index_a = np.where(np.array(atoms.get_chemical_symbols()) == s)[0] index_r = np.where(np.array(slab.get_chemical_symbols()) == s)[0] _, dist = get_distances(p_a[index_a, :], p2=p_r[index_r, :], cell=atoms.cell, pbc=True) # Assume all slab atoms are closest to their reference counterpart. deviations = np.min(dist, axis=1) # Sort deviations. ascending = np.argsort(deviations) # The highest deviations are assumed to be new atoms. ads_atoms += list(index_a[ascending[-symbol_count:]]) # Final check. ua_ads, uc_ads = np.unique(np.array( atoms.get_chemical_symbols())[ads_atoms].sort(), return_counts=True) ua_comp, uc_comp = np.unique(composition.sort(), return_counts=True) if ua_ads != ua_comp: msg = str(ua_ads) + " != " + str(ua_comp) raise AssertionError(msg) elif uc_ads != uc_comp: msg = str(uc_ads) + " != " + str(uc_comp) raise AssertionError(msg) return ads_atoms
def is_desorbed(self): desorbed = False D, D_len = get_distances(self.B.positions, cell=self.B.cell, pbc=True) indexM = np.argmin(D_len[-1, :-1]) dist_S = D_len[-1, indexM] if dist_S > (cradii[self.B[-1].number] + cradii[self.B[indexM].number]) * 2: print('DESORBED FROM SLAB') desorbed = True return desorbed
def test_antisymmetry(): size = 2 atoms = FaceCenteredCubic(size=[size, size, size], symbol='Cu', latticeconstant=2, pbc=(1, 1, 1)) vmin, vlen = get_distances(atoms.get_positions(), cell=atoms.cell, pbc=True) assert (vlen == vlen.T).all() for i, j in itertools.combinations(range(len(atoms)), 2): assert (vmin[i, j] == -vmin[j, i]).all()
def add_CH4_SS(mof, site_idx, ads_pos): """ Add CH4 to the structure from single-site model Args: mof (ASE Atoms object): starting ASE Atoms object of structure site_idx (int): ASE index of site based on single-site model ads_pos (array): 1D numpy array for the best adsorbate position Returns: mof (ASE Atoms object): ASE Atoms object with adsorbate """ #Get CH4 parameters CH4 = molecule('CH4') CH_length = CH4.get_distance(0, 1) CH_angle = CH4.get_angle(1, 0, 2) CH2_dihedral = CH4.get_dihedral(2, 1, 0, 4) CH_length = CH4.get_distance(0, 1) CH_angle = CH4.get_angle(1, 0, 2) CH_dihedral = CH4.get_dihedral(2, 1, 0, 4) #Add CH4 to ideal adsorption position CH4[0].position = ads_pos #Make one of the H atoms colinear with adsorption site and C D, D_len = get_distances([ads_pos], mof[site_idx].position, cell=mof.cell, pbc=mof.pbc) r_vec = D[0, 0] r = (r_vec / np.linalg.norm(r_vec)) * CH_length #Construct rest of CH4 using Z-matrix format CH4[1].position = ads_pos + r CH4.set_distance(0, 2, CH_length, fix=0, mic=True) CH4.set_angle(1, 0, 2, CH_angle) CH4.set_distance(0, 3, CH_length, fix=0, mic=True) CH4.set_angle(1, 0, 3, CH_angle) CH4.set_dihedral(2, 1, 0, 3, -CH_dihedral) CH4.set_distance(0, 4, CH_length, fix=0, mic=True) CH4.set_angle(1, 0, 4, CH_angle) CH4.set_dihedral(2, 1, 0, 4, CH2_dihedral) #Add CH4 molecule to the structure mof.extend(CH4) return mof
def initialize_qm_buffer_mask(self, atoms): """ Initialises system to perform qm calculation """ # calculate the distances between all atoms and qm atoms # qm_distance_matrix is a [N_QM_atoms x N_atoms] matrix _, qm_distance_matrix = get_distances( atoms.positions[self.qm_selection_mask], atoms.positions, atoms.cell, atoms.pbc) self.qm_buffer_mask = np.zeros(len(atoms), dtype=bool) # every r_qm is a matrix of distances # from an atom in qm region and all atoms with size [N_atoms] for r_qm in qm_distance_matrix: self.qm_buffer_mask[r_qm < self.buffer_width] = True
def calc_gofr(bin_edges, axesl, posl): from ase.geometry import get_distances grl = [] for axes, pos in zip(axesl, posl): # get unique pair separations drij, rij = get_distances(pos, cell=axes, pbc=[1, 1, 1]) idx = np.triu_indices(len(rij), k=1) dists = rij[idx] # histogram volume = np.linalg.det(axes) natom = len(pos) gr_norm = gofr_norm(bin_edges, natom, volume) gr1 = gofr_count(bin_edges, dists)*gr_norm grl.append(gr1) grm, gre = yl_ysql(grl) return grm, gre
def slab_indices(slab0, slab1, mask=None): """Match the indices of similar atoms between two slabs.""" n = len(slab0) if mask is None: mask = np.arange(n) matching = np.arange(n) ipos = slab0.positions[mask] fpos = slab1.positions[mask] d = get_distances(ipos, fpos, cell=slab0.cell, pbc=slab0.pbc)[1] matching[mask] = np.argmin(d, axis=1) atoms = sort(slab0, matching) return atoms
def command(self, reference_positions, positions, velocities, previous_positions, previous_velocities, pbc, cell): _, distance_matrix = get_distances(p1=reference_positions, p2=positions, cell=cell, pbc=pbc) closest_reference = np.argmin(distance_matrix, axis=0) is_at_home = (closest_reference == np.arange( len(positions)))[:, np.newaxis] is_away = 1 - is_at_home return { 'positions': is_at_home * positions + is_away * previous_positions, 'velocities': is_at_home * velocities + is_away * -previous_velocities, 'reflected': is_away.astype(bool).flatten() }
def get_interatomic_distances(atoms, D=None, scale=None, direction=None): if D is not None: D[:, :, direction] *= scale D.shape = (-1, 3) distances = np.sqrt((D**2).sum(1)) D.shape = (-1, len(atoms), 3) distances.shape = (-1, len(atoms)) else: D, distances = get_distances(atoms.positions, cell=atoms.cell, pbc=True) min_cell_width = np.min(np.linalg.norm(atoms.cell, axis=1)) min_cell_width *= np.ones(len(atoms)) np.fill_diagonal(distances, min_cell_width) return D, distances
def is_reconstructed(self, xy_cutoff=0.3, z_cutoff=0.4): """Compare initial and final slab configuration to determine if slab reconstructs during relaxation xy_cutoff: Allowed xy-movement is determined from the covalent radii as: xy_cutoff * np.mean(cradii) z_cutoff: Allowed z-movement is determined as z_cutoff * cradii_i """ assert self.A, \ 'Initial slab geometry needed to classify reconstruction' # remove adsorbate A = self.A[:-1].copy() B = self.B[:-1].copy() # Order wrt x-positions x_indices = np.argsort(A.positions[:, 0]) A = A[x_indices] B = B[x_indices] a = A.positions b = B.positions allowed_z_movement = z_cutoff * cradii[A.get_atomic_numbers()] allowed_xy_movement = \ xy_cutoff * np.mean(cradii[A.get_atomic_numbers()]) D, D_len = get_distances(p1=a, p2=b, cell=A.cell, pbc=True) d_xy = np.linalg.norm(np.diagonal(D)[:2], axis=0) d_z = np.diagonal(D)[2:][0] cond1 = np.all(d_xy < allowed_xy_movement) cond2 = np.all([d_z[i] < allowed_z_movement[i] for i in range(len(a))]) if cond1 and cond2: # not reconstructed return False else: return True
def grid_within_cutoff(df,atoms,max_dist,site_pos,partition=1e6): """ Reduces grid dataframe into data within max_dist of active site Args: df (pandas df object): df containing energy grid details (x,y,z,E) atoms (ASE Atoms object): Atoms object of structure max_dist (float): maximum distance from active site to consider site_pos (array): numpy array of the adsorption site partition (float): how many data points to partition the df for. This is used to prevent memory overflow errors. Decrease if memory errors arise. Returns: new_df (pandas df object): modified df only around max_dist from active site and also with a new distance (d) column """ df['d'] = '' n_loops = int(np.ceil(len(df)/partition)) new_df = pd.DataFrame() #Only consider data within max_dist of active site. Cut up the original #dataframe into chunks defined by partition to prevent memory issues for i in range(n_loops): if i == n_loops-1: idx = np.arange(i*int(partition),len(df)) else: idx = np.arange(i*int(partition),(i+1)*int(partition)) D,D_len = get_distances([site_pos],df.loc[idx,['x','y','z']].values, cell=atoms.cell,pbc=atoms.pbc) D_len.shape = (-1,) df.loc[idx,'d'] = D_len new_df = df[df['d'] <= max_dist] return new_df
def add(self): newatoms = self.get_atoms() if newatoms is None: # Error dialog was shown return newcenter = self.getcoords() # Not newatoms.center() because we want the same centering method # used for adding atoms relative to selections (mean). previous_center = newatoms.positions.mean(0) newatoms.positions += newcenter - previous_center atoms = self.gui.atoms if len(atoms) and self.picky.value: from ase.geometry import get_distances disps, dists = get_distances(atoms.positions, newatoms.positions) mindist = dists.min() if mindist < 0.5: ui.showerror( _('Bad positions'), _('Atom would be less than 0.5 Å from ' 'an existing atom. To override, ' 'uncheck the check positions option.')) return atoms += newatoms if len(atoms) > self.gui.images.maxnatoms: self.gui.images.initialize(list(self.gui.images), self.gui.images.filenames) self.gui.images.selected[:] = False # 'selected' array may be longer than current atoms self.gui.images.selected[len(atoms) - len(newatoms):len(atoms)] = True self.gui.set_frame() self.gui.draw()
def sphere(atoms, paths, cutoff): ''' Method for determining valid rings by ensuring that non ring atoms are not within a cutoff distance of the center of mass of the path. ''' from ase.geometry import get_distances valid_paths = [] for p in paths: flag = True if len(p) > 10: ring_atoms = atoms[p] com = ring_atoms.get_center_of_mass() positions = atoms.get_positions() distances = get_distances(com, positions)[1][0] for i, d in enumerate(distances): if d < cutoff and i not in p: flag = False break if flag: valid_paths.append(p) return valid_paths
def atoms_to_graph(atoms,index,max_ring): ''' Helper function to repeat a unit cell enough times to capture the largest possible ring, and turn the new larger cell into a graph object. RETURNS: G = graph object representing zeolite framework in new larger cell large_atoms = ASE atoms object of the new larger cell framework repeat = array showing the number of times the cell was repeated: [x,y,z] ''' # repeat cell, center the cell, and wrap the atoms back into the cell cell = atoms.cell.cellpar()[:3] repeat = [] for i,c in enumerate(cell): if c/2 < max_ring/2+5: l = c re = 1 while l/2 < max_ring/2+5: re += 1 l = c*re repeat.append(re) else: repeat.append(1) large_atoms = atoms.copy() large_atoms = large_atoms.repeat(repeat) center = large_atoms.get_center_of_mass() trans = center - large_atoms.positions[index] large_atoms.translate(trans) large_atoms.wrap() # remove atoms that won't contribute to wrings from ase.geometry import get_distances cell = large_atoms.get_cell() pbc = [1,1,1] p1 = large_atoms[index].position positions = large_atoms.get_positions() distances = get_distances(p1,positions)[1][0] delete = [] for i,l in enumerate(distances): if l>max_ring/2+5: delete.append(i) inds = [atom.index for atom in large_atoms] large_atoms.set_tags(inds) atoms = large_atoms.copy() del large_atoms[delete] matrix = np.zeros([len(large_atoms),len(large_atoms)]).astype(int) positions = large_atoms.get_positions() tsites = [atom.index for atom in large_atoms if atom.symbol != 'O'] tpositions = positions[tsites] osites = [atom.index for atom in large_atoms if atom.index not in tsites] opositions = positions[osites] distances = get_distances(tpositions,opositions)[1] for i,t in enumerate(tsites): dists = distances[i] idx = np.nonzero(dists<2)[0] for o in idx: matrix[t,osites[o]]=1 matrix[osites[o],t]=1 # now we make the graph import networkx as nx G = nx.from_numpy_matrix(matrix) # G.remove_nodes_from(delete) return G, large_atoms, repeat
def distance_pbc(a1, a2, cell, pbc): D, D_len = geometry.get_distances(a1, a2, cell=cell, pbc=pbc) cell_trans = [0, 0, 0] return [D[0, 0, 0], D[1, 1, 1], D[2, 2, 2]], D_len[0, 0], cell_trans
def _match_positions(structure: Atoms, reference: Atoms) -> Tuple[Atoms, float, float]: """Matches the atoms in the input `structure` to the sites in the `reference` structure. The function returns tuple the first element of which is a copy of the `reference` structure, in which the chemical species are assigned to comply with the `structure`. The second and third element of the tuple represent the maximum and average distance between relaxed and reference sites. Parameters ---------- structure structure with relaxed positions reference structure with idealized positions Raises ------ ValueError if the cell metrics of the two input structures do not match ValueError if the periodic boundary conditions of the two input structures do not match ValueError if the input structure contains more atoms than the reference structure """ if not np.all(reference.pbc == structure.pbc): msg = 'The boundary conditions of reference and relaxed structures do not match.' msg += '\n reference: ' + str(reference.pbc) msg += '\n relaxed: ' + str(structure.pbc) raise ValueError(msg) if len(structure) > len(reference): msg = 'The relaxed structure contains more atoms than the reference structure.' msg += '\n reference: ' + str(len(reference)) msg += '\n relaxed: ' + str(len(structure)) raise ValueError(msg) if not np.all(np.isclose(reference.cell, structure.cell)): msg = 'The cell metrics of reference and relaxed structures do not match.' msg += '\n reference: ' + str(reference.cell) msg += '\n relaxed: ' + str(structure.cell) raise ValueError(msg) # compute distances between reference and relaxed positions _, dists = get_distances(reference.positions, structure.positions, cell=reference.cell, pbc=reference.pbc) # pad matrix with zeros to obtain square matrix n, m = dists.shape cost_matrix = np.pad(dists, ((0, 0), (0, abs(n - m))), mode='constant', constant_values=0) # find optimal mapping using Kuhn-Munkres (Hungarian) algorithm row_ind, col_ind = linear_sum_assignment(cost_matrix) # compile new configuration with supplementary information mapped = reference.copy() displacement_magnitudes = [] displacements = [] minimum_distances = [] n_dist_max = min(len(mapped), 3) warning = None for i, j in zip(row_ind, col_ind): atom = mapped[i] if j >= len(structure): # vacant site in reference structure atom.symbol = 'X' displacement_magnitudes.append(None) displacements.append(3 * [None]) minimum_distances.append(n_dist_max * [None]) else: atom.symbol = structure[j].symbol dvecs, drs = get_distances([structure[j].position], [reference[i].position], cell=reference.cell, pbc=reference.pbc) displacement_magnitudes.append(drs[0][0]) displacements.append(dvecs[0][0]) # distances to the next three available sites minimum_distances.append(sorted(dists[:, j])[:n_dist_max]) if len(structure) > 1: if drs[0][0] > min(dists[:, j]) + 1e-6: logger.warning( 'An atom was mapped to a site that was further ' 'away than the closest site (that site was already ' 'occupied by another atom).') warning = 'possible_ambiguity_in_mapping' elif minimum_distances[-1][0] > 0.9 * minimum_distances[-1][1]: logger.warning( 'An atom was approximately equally far from its ' 'two closest sites.') warning = 'possible_ambiguity_in_mapping' displacement_magnitudes = np.array(displacement_magnitudes, dtype=np.float64) mapped.new_array('Displacement', displacements, float, shape=(3, )) mapped.new_array('Displacement_Magnitude', displacement_magnitudes, float) mapped.new_array('Minimum_Distances', minimum_distances, float, shape=(n_dist_max, )) drmax = np.nanmax(displacement_magnitudes) dravg = np.nanmean(displacement_magnitudes) return mapped, drmax, dravg, warning
def add_small_molecules(FF, ff_string): if ff_string == 'TraPPE': SM_constants = small_molecule_constants.TraPPE elif ff_string == 'TIP4P_2005_long': SM_constants = small_molecule_constants.TIP4P_2005_long FF.pair_data['special_bonds'] = 'lj 0.0 0.0 1.0 coul 0.0 0.0 0.0' elif ff_string == 'TIP4P_cutoff': SM_constants = small_molecule_constants.TIP4P_cutoff FF.pair_data['special_bonds'] = 'lj/coul 0.0 0.0 1.0' elif ff_string == 'TIP4P_2005_cutoff': SM_constants = small_molecule_constants.TIP4P_cutoff FF.pair_data['special_bonds'] = 'lj/coul 0.0 0.0 1.0' elif ff_string == 'Ions': SM_constants = small_molecule_constants.Ions FF.pair_data['special_bonds'] = 'lj/coul 0.0 0.0 1.0' # insert more force fields here if needed else: raise ValueError('the small molecule force field', ff_string, 'is not defined') SG = FF.system['graph'] SMG = FF.system['SM_graph'] if len(SMG.nodes()) > 0 and len(SMG.edges()) == 0: print( 'there are no small molecule bonds in the CIF, calculating based on covalent radii...' ) atoms = Atoms() offset = min(SMG.nodes()) for node, data in SMG.nodes(data=True): #print(node, data) atoms.append( Atom(data['element_symbol'], data['cartesian_position'])) atoms.set_cell(FF.system['box']) unit_cell = atoms.get_cell() cutoffs = neighborlist.natural_cutoffs(atoms) NL = neighborlist.NewPrimitiveNeighborList( cutoffs, use_scaled_positions=False, self_interaction=False, skin=0.10) # shorten the cutoff a bit NL.build([True, True, True], unit_cell, atoms.get_positions()) for i in atoms: nbors = NL.get_neighbors(i.index)[0] for j in nbors: bond_length = get_distances(i.position, p2=atoms[j].position, cell=unit_cell, pbc=[True, True, True]) bond_length = np.round(bond_length[1][0][0], 3) SMG.add_edge(i.index + offset, j + offset, bond_length=bond_length, bond_order='1.0', bond_type='S') NMOL = len(list(nx.connected_components(SMG))) print(NMOL, 'small molecules were recovered after bond calculation') mol_flag = 1 max_ind = FF.system['max_ind'] index = max_ind box = FF.system['box'] a, b, c, alpha, beta, gamma = box pi = np.pi ax = a ay = 0.0 az = 0.0 bx = b * np.cos(gamma * pi / 180.0) by = b * np.sin(gamma * pi / 180.0) bz = 0.0 cx = c * np.cos(beta * pi / 180.0) cy = (c * b * np.cos(alpha * pi / 180.0) - bx * cx) / by cz = (c**2.0 - cx**2.0 - cy**2.0)**0.5 unit_cell = np.asarray([[ax, ay, az], [bx, by, bz], [cx, cy, cz]]).T inv_unit_cell = np.linalg.inv(unit_cell) add_nodes = [] add_edges = [] comps = [] for comp in nx.connected_components(SMG): mol_flag += 1 comp = sorted(list(comp)) ID_string = sorted([SMG.nodes[n]['element_symbol'] for n in comp]) ID_string = [(key, len(list(group))) for key, group in groupby(ID_string)] ID_string = ''.join([str(e) for c in ID_string for e in c]) comps.append(ID_string) for n in comp: data = SMG.nodes[n] SMG.nodes[n]['mol_flag'] = str(mol_flag) if ID_string == 'H2O1': SMG.nodes[n][ 'force_field_type'] = SMG.nodes[n]['element_symbol'] + '_w' else: SMG.nodes[n]['force_field_type'] = SMG.nodes[n][ 'element_symbol'] + '_' + ID_string # add COM sites where relevant, extend this list as new types are added if ID_string in ('O2', 'N2'): coords = [] anchor = SMG.nodes[comp[0]]['fractional_position'] for n in comp: data = SMG.nodes[n] data['mol_flag'] = str(mol_flag) fcoord = data['fractional_position'] mic = PBC3DF_sym(fcoord, anchor) fcoord += mic[1] ccoord = np.dot(unit_cell, fcoord) coords.append(ccoord) ccom = np.average(coords, axis=0) fcom = np.dot(inv_unit_cell, ccom) index += 1 if ID_string == 'O2': fft = 'O_com' elif ID_string == 'N2': fft = 'N_com' ndata = { 'element_symbol': 'NA', 'mol_flag': mol_flag, 'index': index, 'force_field_type': fft, 'cartesian_position': ccom, 'fractional_position': fcom, 'charge': 0.0, 'replication': np.array([0.0, 0.0, 0.0]), 'duplicated_version_of': None } edata = {'sym_code': None, 'length': None, 'bond_type': None} add_nodes.append([index, ndata]) add_edges.extend([(index, comp[0], edata), (index, comp[1], edata)]) for n, data in add_nodes: SMG.add_node(n, **data) for e0, e1, data in add_edges: SMG.add_edge(e0, e1, **data) ntypes = max([FF.atom_types[ty] for ty in FF.atom_types]) maxatomtype_wsm = max([FF.atom_types[ty] for ty in FF.atom_types]) maxbondtype_wsm = max([bty for bty in FF.bond_data['params']]) maxangletype_wsm = max([aty for aty in FF.angle_data['params']]) nbonds = max([i for i in FF.bond_data['params']]) nangles = max([i for i in FF.angle_data['params']]) try: ndihedrals = max([i for i in FF.dihedral_data['params']]) except ValueError: ndihedrals = 0 try: nimpropers = max([i for i in FF.improper_data['params']]) except ValueError: nimpropers = 0 new_bond_types = {} new_angle_types = {} new_dihedral_types = {} new_improper_types = {} for subG, ID_string in zip( [SMG.subgraph(c).copy() for c in nx.connected_components(SMG)], comps): constants = SM_constants[ID_string] # add new atom types for name, data in sorted(subG.nodes(data=True), key=lambda x: x[0]): fft = data['force_field_type'] chg = constants['pair']['charges'][fft] data['charge'] = chg SG.add_node(name, **data) try: FF.atom_types[fft] += 0 except KeyError: ntypes += 1 FF.atom_types[fft] = ntypes style = constants['pair']['style'] vdW = constants['pair']['vdW'][fft] FF.pair_data['params'][FF.atom_types[fft]] = (style, vdW[0], vdW[1]) FF.pair_data['comments'][FF.atom_types[fft]] = [fft, fft] FF.atom_masses[fft] = mass_key[data['element_symbol']] if 'hybrid' not in FF.pair_data[ 'style'] and style != FF.pair_data['style']: FF.pair_data['style'] = ' '.join( ['hybrid', FF.pair_data['style'], style]) elif 'hybrid' in FF.pair_data[ 'style'] and style in FF.pair_data['style']: pass elif 'hybrid' in FF.pair_data[ 'style'] and style not in FF.pair_data['style']: FF.pair_data['style'] += ' ' + style # add new bonds used_bonds = [] ty = nbonds for e0, e1, data in subG.edges(data=True): bonds = constants['bonds'] fft_i = SG.nodes[e0]['force_field_type'] fft_j = SG.nodes[e1]['force_field_type'] # make sure the order corresponds to that in the molecule dictionary bond = tuple(sorted([fft_i, fft_j])) try: style = bonds[bond][0] if bond not in used_bonds: ty = ty + 1 new_bond_types[bond] = ty FF.bond_data['params'][ty] = list(bonds[bond]) FF.bond_data['comments'][ty] = list(bond) used_bonds.append(bond) if 'hybrid' not in FF.bond_data[ 'style'] and style != FF.bond_data['style']: FF.bond_data['style'] = ' '.join( ['hybrid', FF.bond_data['style'], style]) elif 'hybrid' in FF.bond_data[ 'style'] and style in FF.bond_data['style']: pass elif 'hybrid' in FF.bond_data[ 'style'] and style not in FF.bond_data['style']: FF.bond_data['style'] += ' ' + style if ty in FF.bond_data['all_bonds']: FF.bond_data['count'] = (FF.bond_data['count'][0] + 1, FF.bond_data['count'][1] + 1) FF.bond_data['all_bonds'][ty].append((e0, e1)) else: FF.bond_data['count'] = (FF.bond_data['count'][0] + 1, FF.bond_data['count'][1] + 1) FF.bond_data['all_bonds'][ty] = [(e0, e1)] except KeyError: pass # add new angles used_angles = [] ty = nangles for name, data in subG.nodes(data=True): angles = constants['angles'] nbors = list(subG.neighbors(name)) for comb in combinations(nbors, 2): j = name i, k = comb fft_i = subG.nodes[i]['force_field_type'] fft_j = subG.nodes[j]['force_field_type'] fft_k = subG.nodes[k]['force_field_type'] angle = sorted((fft_i, fft_k)) angle = (angle[0], fft_j, angle[1]) try: style = angles[angle][0] FF.angle_data['count'] = (FF.angle_data['count'][0] + 1, FF.angle_data['count'][1]) if angle not in used_angles: ty = ty + 1 new_angle_types[angle] = ty FF.angle_data['count'] = (FF.angle_data['count'][0], FF.angle_data['count'][1] + 1) FF.angle_data['params'][ty] = list(angles[angle]) FF.angle_data['comments'][ty] = list(angle) used_angles.append(angle) if 'hybrid' not in FF.angle_data[ 'style'] and style != FF.angle_data['style']: FF.angle_data['style'] = ' '.join( ['hybrid', FF.angle_data['style'], style]) elif 'hybrid' in FF.angle_data[ 'style'] and style in FF.angle_data['style']: pass elif 'hybrid' in FF.angle_data[ 'style'] and style not in FF.angle_data['style']: FF.angle_data['style'] += ' ' + style if ty in FF.angle_data['all_angles']: FF.angle_data['all_angles'][ty].append((i, j, k)) else: FF.angle_data['all_angles'][ty] = [(i, j, k)] except KeyError: pass # add new dihedrals FF.bond_data['count'] = (FF.bond_data['count'][0], len(FF.bond_data['params'])) FF.angle_data['count'] = (FF.angle_data['count'][0], len(FF.angle_data['params'])) if 'tip4p' in FF.pair_data['style']: for ty, pair in FF.pair_data['comments'].items(): fft = pair[0] if fft == 'O_w': FF.pair_data['O_type'] = ty if fft == 'H_w': FF.pair_data['H_type'] = ty for ty, bond in FF.bond_data['comments'].items(): if sorted(bond) == ['H_w', 'O_w']: FF.pair_data['H2O_bond_type'] = ty for ty, angle in FF.angle_data['comments'].items(): if angle == ['H_w', 'O_w', 'H_w']: FF.pair_data['H2O_angle_type'] = ty if 'long' in FF.pair_data['style']: FF.pair_data[ 'M_site_dist'] = 0.1546 # only TIP4P/2005 is implemented elif 'cut' in FF.pair_data[ 'style'] and ff_string == 'TIP4P_2005_cutoff': FF.pair_data['M_site_dist'] = 0.1546 elif 'cut' in FF.pair_data['style'] and ff_string == 'TIP4P_cutoff': FF.pair_data['M_site_dist'] = 0.1500
def cif_read_pymatgen(filename, charges=False, coplanarity_tolerance=0.1): valencies = { 'C': 4.0, 'Si': 4.0, 'Ge': 4.0, 'N': 3.0, 'P': 3.0, 'As': 3.0, 'Sb': 3.0, 'O': 2.0, 'S': 2.0, 'Se': 2.0, 'Te': 2.0, 'F': 1.0, 'Cl': 1.0, 'Br': 1.0, 'I': 1.0, 'H': 1.0, 'X': 1.0 } bond_types = {0.5: 'S', 1.0: 'S', 1.5: 'A', 2.0: 'D', 3.0: 'T'} with open(filename, 'r') as f: f = f.read() f = filter(None, f.split('\n')) charge_list = [] charge_switch = False for line in f: s = line.split() if '_atom_site_charge' in s: charge_switch = True if '_loop' in s: charge_switch = False if len(s) > 5: if charges: if charge_switch: charge_list.append(float(s[-1])) cif = CifParser(filename) struct = cif.get_structures(primitive=False)[0] atoms = AseAtomsAdaptor.get_atoms(struct) unit_cell = atoms.get_cell() inv_uc = inv(unit_cell.T) elements = atoms.get_chemical_symbols() small_skin_metals = ('Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Ba', 'La') if any(i in elements for i in ('Zn')): skin = 0.30 if any(i in elements for i in small_skin_metals): skin = 0.05 else: skin = 0.20 print('skin for bond calculation is', skin) if not charges: charge_list = [0.0 for a in atoms] cutoffs = neighborlist.natural_cutoffs(atoms) NL = neighborlist.NewPrimitiveNeighborList( cutoffs, use_scaled_positions=False, self_interaction=False, skin=skin) # default atom cutoffs work well NL.build([True, True, True], unit_cell, atoms.get_positions()) G = nx.Graph() for a in atoms: G.add_node(a.index, element_symbol=a.symbol, position=a.position) for i in atoms: nbors = NL.get_neighbors(i.index)[0] isym = i.symbol for j in nbors: jsym = atoms[j].symbol if (isym not in metals) and (jsym not in metals) and not any( e == 'X' for e in [isym, jsym]): try: bond = bonds.CovalentBond(struct[i.index], struct[j]) bond_order = bond.get_bond_order() except ValueError: bond_order = 1.0 elif (isym == 'X' or jsym == 'X') and (isym not in metals) and (jsym not in metals): bond_order = 1.0 else: bond_order = 0.5 bond_length = get_distances(i.position, p2=atoms[j].position, cell=unit_cell, pbc=[True, True, True]) bond_length = np.round(bond_length[1][0][0], 3) G.add_edge(i.index, j, bond_length=bond_length, bond_order=bond_order, bond_type='', pymatgen_bond_order=bond_order) NMG = G.copy() edge_list = list(NMG.edges()) for e0, e1 in edge_list: sym0 = NMG.nodes[e0]['element_symbol'] sym1 = NMG.nodes[e1]['element_symbol'] if sym0 in metals or sym1 in metals: NMG.remove_edge(e0, e1) for i, data in G.nodes(data=True): isym = data['element_symbol'] nbors = list(G.neighbors(i)) nbor_symbols = [G.nodes[n]['element_symbol'] for n in nbors] nonmetal_nbor_symbols = [n for n in nbor_symbols if n not in metals] # remove C-M bonds if C is also bonded to carboxylate atoms, these are almost always wrong for n, nsym in zip(nbors, nbor_symbols): if isym == 'C' and sorted(nonmetal_nbor_symbols) == [ 'C', 'O', 'O' ] and nsym in metals: G.remove_edge(i, n) ### intial bond typing, guessed from rounding pymatgen bond orders linkers = nx.connected_components(NMG) aromatic_atoms = [] for linker in linkers: SG = NMG.subgraph(linker) for i, data in SG.nodes(data=True): isym = data['element_symbol'] nbors = list(G.neighbors(i)) nbor_symbols = [G.nodes[n]['element_symbol'] for n in nbors] nonmetal_nbor_symbols = [ n for n in nbor_symbols if n not in metals ] CB = nx.cycle_basis(SG) check_cycles = True if len(CB) < 3: check_cycles = False cyloc = None if check_cycles: for cy in range(len(CB)): if i in CB[cy]: cyloc = cy bond_orders = [] for n, nsym in zip(nbors, nbor_symbols): edge_data = G[n][i] bond_order = edge_data['bond_order'] if bond_order < 1.0 and bond_order != 0.5: bond_order = 1.0 # shortest observed single bond had order 1.321 elif 1.00 <= bond_order < 1.33: bond_order = 1.0 elif 1.33 <= bond_order < 1.75: bond_order = 1.5 # bond orders tend to be on the high end for aromatic compounds elif 1.75 <= bond_order < 2.00: bond_order = 1.5 elif 2.00 <= bond_order < 3.00: bond_order = round(bond_order) # bonds between two disparate cycles or cycles and non-cycles should have order 1.0 if check_cycles and cyloc != None: if n not in CB[cyloc]: bond_order = 1.0 if any(i in metals for i in nbor_symbols) and isym == 'O' and nsym == 'C': bond_order = 1.5 if nsym in metals: bond_order = 0.5 if isym == 'C' and len(nbor_symbols) == 4: bond_order = 1.0 if isym == 'C' and sorted(nbor_symbols) == ['C', 'O', 'O']: if nsym == 'C': bond_order = 1.0 elif nsym == 'O': bond_order = 1.5 else: pass edge_data['bond_order'] = bond_order bond_orders.append(bond_order) edge_data['bond_type'] = bond_types[bond_order] all_cycles = nx.simple_cycles(nx.to_directed(SG)) all_cycles = set( [tuple(sorted(cy)) for cy in all_cycles if len(cy) > 4]) ### assign aromatic bond orders as 1.5 (in most cases they will be already) for cycle in all_cycles: # rotate the ring normal vec onto the z-axis to determine coplanarity coords = np.array([G.nodes[c]['position'] for c in cycle]) fcoords = np.dot(inv_uc, coords.T).T anchor = fcoords[0] fcoords = np.array( [vec - PBC3DF_sym(anchor, vec)[1] for vec in fcoords]) coords = np.dot(unit_cell.T, fcoords.T).T coords -= np.average(coords, axis=0) vec0 = coords[0] vec1 = coords[1] normal = np.cross(vec0, vec1) RZ = M(normal, np.array([0.0, 0.0, 1.0])) coords = np.dot(RZ, coords.T).T maxZ = max([abs(z) for z in coords[:, -1]]) # if coplanar make all bond orders 1.5 if maxZ < coplanarity_tolerance: aromatic_atoms.extend(list(cycle)) cycle_subgraph = SG.subgraph(cycle) for e0, e1 in cycle_subgraph.edges(): G[e0][e1]['bond_order'] = 1.5 for i, data in G.nodes(data=True): isym = data['element_symbol'] bond_orders = [G[i][n]['bond_order'] for n in G.neighbors(i)] total_bond_order = np.sum(bond_orders) bond_orders = [str(o) for o in bond_orders] nbor_symbols = ' '.join( [G.nodes[n]['element_symbol'] for n in G.neighbors(i)]) if isym not in metals and total_bond_order != valencies[isym]: message = ' '.join([ str(isym), 'has total bond order', str(total_bond_order), 'with neighbors', nbor_symbols, 'and bond orders' ] + bond_orders) warnings.warn(message) elems = atoms.get_chemical_symbols() names = [a.symbol + str(a.index) for a in atoms] ccoords = atoms.get_positions() fcoords = atoms.get_scaled_positions() bond_list = [] for e0, e1, data in G.edges(data=True): sym0 = G.nodes[e0]['element_symbol'] sym1 = G.nodes[e1]['element_symbol'] name0 = sym0 + str(e0) name1 = sym1 + str(e1) bond_list.append( [name0, name1, '.', data['bond_type'], data['bond_length']]) A, B, C = unit_cell.lengths() alpha, beta, gamma = unit_cell.angles() return elems, names, ccoords, fcoords, charge_list, bond_list, ( A, B, C, alpha, beta, gamma), np.asarray(unit_cell).T
step = 0 file_2C = open(data_base + str("2C.dat"), "w") file_3C = open(data_base + str("3C.dat"), "w") file_4C = open(data_base + str("4C.dat"), "w") file_2O = open(data_base + str("2O.dat"), "w") file_3O = open(data_base + str("3O.dat"), "w") # Build descriptors from positions (train set only) sigma_ = 0.3 # 3*sigma ~ 2.7A relatively large spread cutoff_ = 3.5 # cut_off SOAP, nmax_ = 3 lmax_ = 3 distances = asegeom.get_distances(traj[step].positions, pbc=traj[step].pbc, cell=traj[step].cell)[1] soaps = desc.createDescriptorsSingleSOAP(traj[step], species, sigma_, cutoff_, nmax_, lmax_, periodic) cut_off = 1.75 cut_low = 1.6 cut_high = 1.9 to_ignore_mask = np.array([ sum((distances[atom, :] > cut_low) & (distances[atom, :] < cut_high)) for atom in range(n_atoms) ]) < 1 label_naive = np.array( [sum(distances[atom, :] < cut_off) - 1 for atom in range(n_atoms)]) max_neighbours = np.amax(