def get_primitive_cell(atoms, tol=1e-8): """Atoms object interface with spglib primitive cell finder: https://atztogo.github.io/spglib/python-spglib.html#python-spglib Parameters ---------- atoms : object Atoms object to search for a primitive unit cell. tol : float Tolerance for floating point rounding errors. Returns ------- primitive cell : object The primitive unit cell returned by spglib if one is found. """ lattice = atoms.cell positions = atoms.get_scaled_positions() numbers = atoms.get_atomic_numbers() cell = (lattice, positions, numbers) cell = spglib.find_primitive(cell, symprec=tol) if cell is None: return None _lattice, _positions, _numbers = cell atoms = Gratoms(symbols=_numbers, cell=_lattice, pbc=atoms.pbc) atoms.set_scaled_positions(_positions) return atoms
def molecule_search(self, element_pool={ 'C': 2, 'H': 6 }, load_molecules=True, multiple_bond_search=False): """Return the enumeration of molecules which can be produced from the specified atoms. Parameters ---------- element_pool : dict Atomic symbols keys paired with the maximum number of that atom. load_molecules : bool Load any existing molecules from the database. multiple_bond_search : bool Allow atoms to form bonds with other atoms in the molecule. """ numbers = np.zeros(len(self.base_valence)) for k, v in element_pool.items(): numbers[chemical_symbols.index(k)] = v self.element_pool = numbers self.multiple_bond_search = multiple_bond_search molecules = {} if load_molecules: self.molecules = self.load_molecules(binned=True) search_molecules = [] for el, cnt in enumerate(self.element_pool): if cnt == 0: continue molecule = Gratoms(symbols=[el]) molecule.nodes[0]['valence'] = self.base_valence[el] search_molecules += [molecule] comp_tag, bond_tag = molecule.get_chemical_tags() if comp_tag not in self.molecules: molecules[comp_tag] = {bond_tag: [molecule]} for molecule in search_molecules: # Recusive use of molecules new_molecules, molecules = self._branch_molecule( molecule, molecules) search_molecules += new_molecules self.save_molecules(molecules)
def to_gratoms(atoms): """Convert and atom object to a gratoms object.""" gratoms = Gratoms(numbers=atoms.numbers, positions=atoms.positions, pbc=atoms.pbc, cell=atoms.cell) if atoms.constraints: gratoms.set_constraint(atoms.constraints) return gratoms
def root_surface(self, slab, root, vector=None): """Creates a cell from a primitive cell that repeats along the x and y axis in a way consistent with the primitive cell, that has been cut to have a side length of *root*. *primitive cell* should be a primitive 2d cell of your slab, repeated as needed in the z direction. *root* should be determined using an analysis tool such as the root_surface_analysis function, or prior knowledge. It should always be a whole number as it represents the number of repetitions. """ atoms = slab.copy() xynorm = norm(atoms.cell[0:2, 0:2], axis=1) vectors = atoms.cell[0:2, 0:2] / xynorm[0] mvectors = norm(vectors, axis=1) vnorm = norm(vector) angle = -np.arctan2(vector[1], vector[0]) scale = vnorm / mvectors[0] maxn = int(np.ceil(scale)) * 2 atoms *= (maxn, maxn, 1) atoms.cell[0:2, 0:2] = slab.cell[0:2, 0:2] * scale atoms.rotate((angle * 180) / np.pi, 'z') coords = atoms.get_scaled_positions() original_index = np.arange(coords.shape[0]) periodic_match = original_index.copy() for i, j in enumerate(periodic_match): if i != j: continue matched = geometry.matching_sites(coords[i], coords) periodic_match[matched] = i repeated = np.where(periodic_match != original_index) del atoms[repeated] atoms.wrap() ind = np.lexsort( (atoms.positions[:, 0], atoms.positions[:, 1], atoms.positions[:, 2])) atoms = Gratoms(positions=atoms.positions[ind], numbers=atoms.numbers[ind], cell=atoms.cell, pbc=atoms.pbc, tags=atoms.get_tags()) assert (len(atoms) == len(slab) * root) return atoms
def id_adsorbates(self, classifier='radial', return_atoms=False): """Return a list of Gratoms objects for each adsorbate classified on a surface. Requires classification of adsorbate atoms. Parameters ---------- classifier : str Classification technique to identify individual adsorbates. 'radial': Use standard cutoff distances to identify neighboring atoms. return_atoms : bool Return Gratoms objects instead of adsorbate indices. Returns ------- adsorabtes : list (n,) Adsorbate indices of adsorbates in unit cell. """ atoms = self.atoms.copy() # Remove the slab atoms ads_atoms = self.ads_atoms if ads_atoms is None: ads_atoms = self.id_adsorbate_atoms() if classifier == 'radial': con = utils.get_cutoff_neighbors(atoms) ads_con = con[ads_atoms][:, ads_atoms] G = nx.Graph() G.add_nodes_from(ads_atoms) edges = utils.connectivity_to_edges(ads_con, indices=ads_atoms) G.add_weighted_edges_from(edges, weight='bonds') SG = nx.connected_component_subgraphs(G) adsorabtes = [] for sg in SG: nodes = list(sg.nodes) if return_atoms: edges = list(sg.edges) ads = Gratoms(numbers=atoms.numbers[nodes], positions=atoms.positions[nodes], edges=edges) ads.center(vacuum=5) else: ads = nodes adsorabtes += [ads] return adsorabtes
def test_gratoms(): edges = [(0, 1), (0, 2)] atoms = Gratoms(edges=edges) for n in atoms.edges: assert (n in edges) mol = molecule('H2O') atoms = to_gratoms(mol) atoms.graph.add_edges_from([(0, 1), (0, 2)]) sym_test = atoms.get_neighbor_symbols(0) assert (sym_test.tolist() == ['H', 'H']) test_tags = atoms.get_chemical_tags(rank=1) assert (test_tags == '2,0,0,0,0,0,0,1') test_comp, test_bonds = atoms.get_chemical_tags() assert (test_comp == '2,0,0,0,0,0,0,1') assert (test_bonds == '4,0,0,0,0,0,0,3') atoms.set_constraint(FixAtoms(indices=[0])) del atoms[2] assert (len(atoms) == 2) nx.set_node_attributes(atoms.graph, name='valence', values={0: 1, 1: 0}) test_nodes = atoms.get_unsaturated_nodes(screen=1) assert (test_nodes == [0])
def rdkit_to_gratoms(rdkG, name=None, confid=-1): """TODO: conserve 3D positions if present.""" block = Chem.MolToMolBlock(rdkG, confId=confid) positions = np.empty((rdkG.GetNumAtoms(), 3)) n = rdkG.GetNumAtoms() symbols, edges, valence = [], [], {} for i, atom in enumerate(block.split('\n')[4:n + 4]): data = atom.split() positions[i] = np.array(data[:3], dtype=float) symbols += [data[3]] valence.update({i: int(data[9])}) for i, bond in enumerate(block.split('\n')[n + 4:]): data = bond.split() if data[0] == 'M': break data = np.array(data, dtype=int) edges += [(data[0] - 1, data[1] - 1, {'bonds': data[2]})] gratoms = Gratoms(symbols, positions) gratoms.graph.name = name nx.set_node_attributes(gratoms.graph, values=valence, name='valence') gratoms.graph.add_edges_from(edges) return gratoms
def test_edge_addition(self): """Test that edges are added correctly.""" edges = [(0, 1), (0, 2)] atoms = Gratoms(edges=edges) for n in atoms.edges: assert (n in edges)
def _build_basis(self, bulk): """Get the basis unit cell from bulk unit cell. This basis is effectively the same as the bulk, but rotated such that the z-axis is aligned with the surface termination. The basis is stored separately from the slab generated and is only intended for internal use. Returns ------- basis : atoms object The basis slab corresponding to the provided bulk. """ del bulk.constraints if len(np.nonzero(self.miller_index)[0]) == 1: mi = max(np.abs(self.miller_index)) basis = circulant(self.miller_index[::-1] / mi).astype(int) else: h, k, l = self.miller_index p, q = ext_gcd(k, l) a1, a2, a3 = bulk.cell k1 = np.dot(p * (k * a1 - h * a2) + q * (l * a1 - h * a3), l * a2 - k * a3) k2 = np.dot(l * (k * a1 - h * a2) - k * (l * a1 - h * a3), l * a2 - k * a3) if abs(k2) > self.tol: i = -int(np.round(k1 / k2)) p, q = p + i * l, q - i * k a, b = ext_gcd(p * k + q * l, h) c1 = (p * k + q * l, -p * h, -q * h) c2 = np.array((0, l, -k)) // abs(gcd(l, k)) c3 = (b, a * p, a * q) basis = np.array([c1, c2, c3]) basis_atoms = Gratoms(positions=bulk.positions, numbers=bulk.get_atomic_numbers(), cell=bulk.cell, pbc=True) scaled = solve(basis.T, basis_atoms.get_scaled_positions().T).T scaled -= np.floor(scaled + self.tol) basis_atoms.set_scaled_positions(scaled) basis_atoms.set_cell(np.dot(basis, basis_atoms.cell), scale_atoms=True) a1, a2, a3 = basis_atoms.cell n1 = np.cross(a1, a2) a3 = n1 / norm(n1) rotate(basis_atoms, a3, (0, 0, 1), a1, (1, 0, 0)) return basis_atoms
def get_spglib_cell(atoms, primitive=False, idealize=True, tol=1e-5): """Atoms object interface with spglib primitive cell finder: https://atztogo.github.io/spglib/python-spglib.html#python-spglib Parameters ---------- atoms : object Atoms object to search for a primitive unit cell. primitive : bool Reduce the atoms object into a primitive form. idealize : bool Convert the cell into the spglib standardized form. tol : float Tolerance for floating point rounding errors. Returns ------- primitive cell : object The primitive unit cell returned by spglib if one is found. """ lattice = atoms.cell positions = atoms.get_scaled_positions() numbers = atoms.get_atomic_numbers() cell = (lattice, positions, numbers) cell = spglib.standardize_cell(cell, to_primitive=primitive, no_idealize=~idealize, symprec=tol) if cell is None: return atoms _lattice, _positions, _numbers = cell atoms = Gratoms(symbols=_numbers, cell=_lattice, pbc=atoms.pbc) atoms.set_scaled_positions(_positions) return atoms
def hydrogenate(atoms, bins, copy=True): """Add hydrogens to a gratoms object via provided bins""" h_index = len(atoms) edges = [] for i, j in enumerate(bins): for _ in range(j): edges += [(i, h_index)] h_index += 1 if copy: atoms = atoms.copy() atoms += Gratoms('H{}'.format(sum(bins))) atoms.graph.add_edges_from(edges) return atoms
def structure_to_atoms( self, structure, parameters=None): """Return an atoms object for a given structure from the database. Parameters ---------- structure : Structure object A structure from the CatFlow database. parameters : dict Parameters used to perform the calculation. If None, they will be retrieved from the database. Returns ------- atoms : Gratoms object Atomic structure. """ if parameters is None: calculator_name, parameters = self.cursor.query( Calculator.name, Calculator.parameters).\ filter(Calculator.id == structure.calculator_id).one() else: calculator_name = parameters.pop('calculator_name') calculator = utils.str_to_class(calculator_name) atoms = Gratoms( numbers=structure.numbers, positions=structure.positions, pbc=structure.pbc, cell=structure.cell) results = {} for prop_name in utils.supported_properties: prop = getattr(structure, prop_name) if isinstance(prop, list): prop = np.array(prop) results[prop_name] = prop calculator = calculator(atoms, **parameters) calculator.set_results(results) return atoms
def load_molecules(self, ids=None, binned=False): """Load 2D molecule graphs from the database. Parameters ---------- binned : bool Return the molecules in sub-dictionaries of their corresponding composition and bonding tags. Returns ------- molecules : dict All molecules present in the database. """ if isinstance(ids, list): ids = ','.join([str(_) for _ in ids]) cmd = """SELECT m.molecule_pid, m.comp_tag, m.bond_tag, nodes, bonds FROM molecules m LEFT JOIN ( SELECT molecule_id, GROUP_CONCAT(node_id || ',' || atom_num || ',' || valence, ';') as nodes FROM atoms GROUP BY molecule_id ) a ON a.molecule_id = m.molecule_pid LEFT JOIN ( SELECT molecule_id, GROUP_CONCAT(node_id1 || ',' || node_id2 || ',' || nbonds, ';') as bonds FROM bonds GROUP BY molecule_id ) b ON b.molecule_id = m.molecule_pid """ if ids: cmd += """WHERE m.molecule_pid IN ({})""".format(ids) self.c.execute(cmd) fetch = self.c.fetchall() molecules = {} for index, comp_tag, bond_tag, node_data, edge_data in fetch: # Unpacks node, number, and valence node_data = np.array([_.split(',') for _ in node_data.split(';')], dtype=int) data, symbols = {}, [] for node, n, valence in node_data: data.update({node: valence}) symbols += [n] molecule = Gratoms(symbols) molecule.graph.name = index nx.set_node_attributes(molecule.graph, data, 'valence') if edge_data: edges = np.array([_.split(',') for _ in edge_data.split(';')], dtype=int) molecule.graph.add_weighted_edges_from(edges, weight='bonds') if binned: if comp_tag not in molecules: molecules[comp_tag] = {} if bond_tag not in molecules[comp_tag]: molecules[comp_tag][bond_tag] = [] molecules[comp_tag][bond_tag] += [molecule] else: molecules[index] = molecule return molecules
def align_crystal(self, bulk, miller_index): """Return an aligned unit cell from bulk unit cell. This alignment rotates the a and b basis vectors to be parallel to the Miller index. Parameters ---------- bulk : Atoms object Bulk system to be standardized. miller_index : list (3,) Miller indices to align with the basis vectors. Returns ------- new_bulk : Gratoms object Standardized bulk unit cell. """ del bulk.constraints if len(np.nonzero(miller_index)[0]) == 1: mi = max(np.abs(miller_index)) new_lattice = scipy.linalg.circulant(miller_index[::-1] / mi).astype(int) else: h, k, l = miller_index p, q = utils.ext_gcd(k, l) a1, a2, a3 = bulk.cell k1 = np.dot(p * (k * a1 - h * a2) + q * (l * a1 - h * a3), l * a2 - k * a3) k2 = np.dot(l * (k * a1 - h * a2) - k * (l * a1 - h * a3), l * a2 - k * a3) if abs(k2) > self.tol: i = -int(np.round(k1 / k2)) p, q = p + i * l, q - i * k a, b = utils.ext_gcd(p * k + q * l, h) c1 = (p * k + q * l, -p * h, -q * h) c2 = np.array((0, l, -k)) // abs(gcd(l, k)) c3 = (b, a * p, a * q) new_lattice = np.array([c1, c2, c3]) scaled = np.linalg.solve(new_lattice.T, bulk.get_scaled_positions().T).T scaled -= np.floor(scaled + self.tol) new_bulk = Gratoms(positions=bulk.positions, numbers=bulk.get_atomic_numbers(), pbc=True) if not self.attach_graph: del new_bulk._graph new_bulk.set_scaled_positions(scaled) new_bulk.set_cell(np.dot(new_lattice, bulk.cell), scale_atoms=True) # Align the longest of the ab basis vectors with x d = np.linalg.norm(new_bulk.cell[:2], axis=1) if d[1] > d[0]: new_bulk.cell[[0, 1]] = new_bulk.cell[[1, 0]] a = new_bulk.cell[0] a3 = np.cross(a, new_bulk.cell[1]) / np.max(d) rotate(new_bulk, a3, (0, 0, 1), a, (1, 0, 0)) # Ensure the remaining basis vectors are positive in their # corresponding axis for i in range(1, 3): if new_bulk.cell[i][i] < 0: new_bulk.cell[i] *= -1 new_bulk.wrap(eps=1e-3) return new_bulk
def get_topologies(symbols, saturate=False): """Return the possible topologies of a given chemical species Parameters ---------- symbols : str Atomic symbols to construct the topologies from. saturate : bool Saturate the molecule with hydrogen based on the default.radicals set. Returns ------- """ num, cnt = utils.get_atomic_numbers(symbols, True) mcnt = cnt[num != 1] mnum = num[num != 1] if cnt[num == 1]: hcnt = cnt[num == 1][0] else: hcnt = 0 elements = np.repeat(mnum, mcnt) max_degree = defaults.get('radicals')[elements] n = mcnt.sum() hmax = int(max_degree.sum() - (n - 1) * 2) if hcnt > hmax: hcnt = hmax if saturate: hcnt = hmax if n == 1: atoms = Gratoms(elements, cell=[1, 1, 1]) hatoms = hydrogenate(atoms, np.array([hcnt])) return [hatoms] elif n == 0: hatoms = Gratoms('H{}'.format(hcnt)) if hcnt == 2: hatoms.graph.add_edge(0, 1, bonds=1) return [hatoms] ln = np.arange(n).sum() il = np.tril_indices(n, -1) backbones, molecules = [], [] combos = itertools.combinations(np.arange(ln), n - 1) for c in combos: # Construct the connectivity matrix ltm = np.zeros(ln) ltm[[c]] = 1 connectivity = np.zeros((n, n)) connectivity[il] = ltm connectivity = np.maximum(connectivity, connectivity.T) degree = connectivity.sum(axis=0) # Not fully connected (cyclical subgraph) if np.any(degree == 0): continue # Overbonded atoms. remaining_bonds = (max_degree - degree).astype(int) if np.any(remaining_bonds < 0): continue atoms = Gratoms(numbers=elements, edges=connectivity, cell=[1, 1, 1]) isomorph = False for G0 in backbones: if atoms.is_isomorph(G0): isomorph = True break if not isomorph: backbones += [atoms] # The backbone is saturated, do not enumerate if hcnt == hmax: hatoms = hydrogenate(atoms, remaining_bonds) molecules += [hatoms] continue # Enumerate hydrogens across backbone for bins in bin_hydrogen(hcnt, n): if not np.all(bins <= remaining_bonds): continue hatoms = hydrogenate(atoms, bins) isomorph = False for G0 in molecules: if hatoms.is_isomorph(G0): isomorph = True break if not isomorph: molecules += [hatoms] return molecules
def load_3d_structures(self, ids=None): """Return Gratoms objects from the ReactionNetwork database. Parameters ---------- ids : int or list of int Identifier of the molecule in the database. If None, return all structure. Returns ------- images : list All Gratoms objects in the database. """ if isinstance(ids, list): ids = ','.join([str(_) for _ in ids]) if ids is None: cmd = """SELECT GROUP_CONCAT(x_coord || ',' || y_coord || ',' || z_coord, ';'), GROUP_CONCAT(symbol, ';') FROM positions GROUP BY molecule_id """ self.c.execute(cmd) fetch = self.c.fetchall() images = [] for i, out in enumerate(fetch): symbols = out[1].split(';') positions = np.array([_.split(',') for _ in out[0].split(';')], dtype=float) gratoms = Gratoms(symbols, positions) gratoms.graph.name = i images += [gratoms] return images else: cmd = """SELECT GROUP_CONCAT(x_coord || ',' || y_coord || ',' || z_coord, ';'), GROUP_CONCAT(symbol, ';') FROM positions WHERE molecule_id IN ({}) GROUP BY molecule_id """.format(ids) self.c.execute(cmd) out = self.c.fetchone() if out is None: raise ValueError('No matching index found') symbols = out[1].split(';') positions = np.array([_.split(',') for _ in out[0].split(';')], dtype=float) gratoms = Gratoms(symbols, positions) gratoms.graph.name = int(ids) return gratoms
def get_slab(self, size=(1, 1), root=None, iterm=None, primitive=False): """Generate a slab object with a certain number of layers. Parameters ---------- size : tuple (2,) Repeat the x and y lattice vectors by the indicated dimensions root : int Produce a slab with a primitive a1 basis vector multiplied by the square root of a provided value. Uses primitive unit cell. iterm : int A termination index in reference to the list of possible terminations. primitive : bool Whether to reduce the unit cell to its primitive form. Returns ------- slab : atoms object The modified basis slab produced based on the layer specifications given. """ slab = self._basis.copy() if iterm: if self.unique_terminations is None: terminations = self.get_unique_terminations() else: terminations = self.unique_terminations zshift = terminations[iterm] slab.translate([0, 0, -zshift]) slab.wrap(pbc=True) # Get the minimum number of layers needed zlayers = utils.get_unique_coordinates(slab, direct=False, tol=self.tol) if self.min_width: width = slab.cell[2][2] z_repetitions = np.ceil(width / len(zlayers) * self.min_width) else: z_repetitions = np.ceil(self.layers / len(zlayers)) slab *= (1, 1, int(z_repetitions)) if primitive or root: if self.vacuum: slab.center(vacuum=self.vacuum, axis=2) else: raise ValueError('Primitive slab generation requires vacuum') nslab = utils.get_primitive_cell(slab) if nslab is not None: slab = nslab # spglib occasionally returns a split slab zpos = slab.get_scaled_positions() if zpos[:, 2].max() > 0.9 or zpos[:, 2].min() < 0.1: zpos[:, 2] -= 0.5 zpos[:, 2] %= 1 slab.set_scaled_positions(zpos) slab.center(vacuum=self.vacuum, axis=2) # For hcp(1, 1, 0), primitive alters z-axis d = norm(slab.cell, axis=0) maxd = np.argwhere(d == d.max())[0][0] if maxd != 2: slab.rotate(slab.cell[maxd], 'z', rotate_cell=True) slab.cell[[maxd, 2]] = slab.cell[[2, maxd]] slab.cell[maxd] = -slab.cell[maxd] slab.wrap(pbc=True) slab.rotate(slab.cell[0], 'x', rotate_cell=True) # Orthogonalize the z-coordinate # Warning: bulk symmetry is lost at this point a, b, c = slab.cell nab = np.cross(a, b) c = (nab * np.dot(c, nab) / norm(nab)**2) slab.cell[2] = c # Align the longest remaining basis vector with x vdist = norm(slab.cell[:2], axis=1) if vdist[1] > vdist[0]: slab.rotate(slab.cell[1], 'x', rotate_cell=True) slab.cell[0] *= -1 slab.cell[[0, 1]] = slab.cell[[1, 0]] # Enforce that the angle between basis vectors is acute. if slab.cell[1][0] < 0: slab.rotate(slab.cell[2], '-z') slab.cell *= [[1, 0, 0], [-1, 1, 0], [0, 0, 1]] if root: roots, vectors = root_surface_analysis(slab, return_vectors=True) if root not in roots: raise ValueError( 'Requested root structure unavailable for this system.' 'Try: {}'.format(roots)) vect = vectors[np.where(root == roots)][0] slab = self.root_surface(slab, root, vect) # Get the direct z-coordinate of the requested layer zlayers = utils.get_unique_coordinates(slab, direct=False, tag=True, tol=self.tol) if not self.fix_stoichiometry: reverse_sort = np.sort(zlayers)[::-1] if self.min_width: n = np.where(zlayers < self.min_width, 1, 0).sum() ncut = reverse_sort[n] else: ncut = reverse_sort[:self.layers][-1] zpos = slab.positions[:, 2] index = np.arange(len(slab)) del slab[index[zpos - ncut < -self.tol]] slab.cell[2][2] -= ncut slab.translate([0, 0, -ncut]) slab *= (size[0], size[1], 1) tags = slab.get_tags() m = np.where(tags == 1)[0][0] translation = slab[m].position.copy() translation[2] = 0 slab.translate(-translation) slab.wrap() ind = np.lexsort( (slab.positions[:, 0], slab.positions[:, 1], slab.positions[:, 2])) slab = Gratoms(positions=slab.positions[ind], numbers=slab.numbers[ind], cell=slab.cell, pbc=[1, 1, 0], tags=tags[ind]) fix = tags.max() - self.fixed constraints = FixAtoms(indices=[a.index for a in slab if a.tag > fix]) slab.set_constraint(constraints) if self.vacuum: slab.center(vacuum=self.vacuum, axis=2) self.slab = slab return slab